Skip to content

Commit 3907017

Browse files
committed
feat(indexes): make sync-v1 indexes optional
1 parent 49cd156 commit 3907017

File tree

9 files changed

+217
-86
lines changed

9 files changed

+217
-86
lines changed

hathor/indexes/height_index.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
# limitations under the License.
1414

1515
from abc import abstractmethod
16-
from typing import List, NamedTuple, Optional, Tuple
16+
from typing import TYPE_CHECKING, List, NamedTuple, Optional, Tuple
1717

1818
from hathor.indexes.base_index import BaseIndex
1919
from hathor.indexes.scope import Scope
2020
from hathor.transaction import BaseTransaction, Block
2121
from hathor.transaction.genesis import BLOCK_GENESIS
2222
from hathor.util import not_none
2323

24+
if TYPE_CHECKING: # pragma: no cover
25+
from hathor.transaction.storage import TransactionStorage
26+
2427
SCOPE = Scope(
2528
include_blocks=True,
2629
include_txs=False,
@@ -78,6 +81,18 @@ def get(self, height: int) -> Optional[bytes]:
7881
"""
7982
raise NotImplementedError
8083

84+
def find_by_timestamp(self, timestamp: float, tx_storage: 'TransactionStorage') -> Optional[Block]:
85+
""" This method starts from the tip and advances to the parent until it finds a block with lower timestamp.
86+
"""
87+
# TODO: optimize
88+
if timestamp < BLOCK_GENESIS.timestamp:
89+
return None
90+
block = tx_storage.get_transaction(self.get_tip())
91+
assert isinstance(block, Block)
92+
while block.timestamp > timestamp:
93+
block = block.get_block_parent()
94+
return block
95+
8196
@abstractmethod
8297
def get_tip(self) -> bytes:
8398
""" Return the best block hash, it returns the genesis when there is no other block

hathor/indexes/manager.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,12 @@ class IndexesManager(ABC):
5353
log = get_logger()
5454

5555
info: InfoIndex
56-
all_tips: TipsIndex
57-
block_tips: TipsIndex
58-
tx_tips: TipsIndex
59-
56+
all_tips: Optional[TipsIndex]
57+
block_tips: Optional[TipsIndex]
58+
tx_tips: Optional[TipsIndex]
6059
sorted_all: TimestampIndex
6160
sorted_blocks: TimestampIndex
6261
sorted_txs: TimestampIndex
63-
6462
height: HeightIndex
6563
deps: Optional[DepsIndex]
6664
mempool_tips: Optional[MempoolTipsIndex]
@@ -98,6 +96,11 @@ def iter_all_indexes(self) -> Iterator[BaseIndex]:
9896
self.utxo,
9997
])
10098

99+
@abstractmethod
100+
def enable_tips_indexes(self) -> None:
101+
"""Enable tips indexs. It does nothing if it has already been enabled."""
102+
raise NotImplementedError
103+
101104
@abstractmethod
102105
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
103106
"""Enable address index. It does nothing if it has already been enabled."""
@@ -209,18 +212,21 @@ def add_tx(self, tx: BaseTransaction) -> bool:
209212

210213
# These two calls return False when a transaction changes from
211214
# voided to executed and vice-versa.
212-
r1 = self.all_tips.add_tx(tx)
213-
r2 = self.sorted_all.add_tx(tx)
214-
assert r1 == r2
215+
r1 = self.sorted_all.add_tx(tx)
216+
if self.all_tips is not None:
217+
r2 = self.all_tips.add_tx(tx)
218+
assert r1 == r2
215219

216220
if tx.is_block:
217-
r3 = self.block_tips.add_tx(tx)
218-
r4 = self.sorted_blocks.add_tx(tx)
219-
assert r3 == r4
221+
r3 = self.sorted_blocks.add_tx(tx)
222+
if self.block_tips is not None:
223+
r4 = self.block_tips.add_tx(tx)
224+
assert r3 == r4
220225
else:
221-
r3 = self.tx_tips.add_tx(tx)
222-
r4 = self.sorted_txs.add_tx(tx)
223-
assert r3 == r4
226+
r3 = self.sorted_txs.add_tx(tx)
227+
if self.tx_tips is not None:
228+
r4 = self.tx_tips.add_tx(tx)
229+
assert r3 == r4
224230

225231
if self.addresses:
226232
self.addresses.add_tx(tx)
@@ -250,7 +256,8 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
250256
# We delete from indexes in two cases: (i) mark tx as voided, and (ii) remove tx.
251257
# We only remove tx from all_tips and sorted_all when it is removed from the storage.
252258
# For clarity, when a tx is marked as voided, it is not removed from all_tips and sorted_all.
253-
self.all_tips.del_tx(tx, relax_assert=relax_assert)
259+
if self.all_tips is not None:
260+
self.all_tips.del_tx(tx, relax_assert=relax_assert)
254261
self.sorted_all.del_tx(tx)
255262
if self.addresses:
256263
self.addresses.remove_tx(tx)
@@ -264,11 +271,13 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
264271
self.mempool_tips.update(tx, remove=True)
265272

266273
if tx.is_block:
267-
self.block_tips.del_tx(tx, relax_assert=relax_assert)
268274
self.sorted_blocks.del_tx(tx)
275+
if self.block_tips is not None:
276+
self.block_tips.del_tx(tx, relax_assert=relax_assert)
269277
else:
270-
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
271278
self.sorted_txs.del_tx(tx)
279+
if self.tx_tips is not None:
280+
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
272281

273282
if self.tokens:
274283
self.tokens.del_tx(tx)
@@ -283,12 +292,11 @@ def __init__(self) -> None:
283292
from hathor.indexes.memory_height_index import MemoryHeightIndex
284293
from hathor.indexes.memory_info_index import MemoryInfoIndex
285294
from hathor.indexes.memory_timestamp_index import MemoryTimestampIndex
286-
from hathor.indexes.memory_tips_index import MemoryTipsIndex
287295

288296
self.info = MemoryInfoIndex()
289-
self.all_tips = MemoryTipsIndex(scope_type=TipsScopeType.ALL)
290-
self.block_tips = MemoryTipsIndex(scope_type=TipsScopeType.BLOCKS)
291-
self.tx_tips = MemoryTipsIndex(scope_type=TipsScopeType.TXS)
297+
self.all_tips = None
298+
self.block_tips = None
299+
self.tx_tips = None
292300

293301
self.sorted_all = MemoryTimestampIndex(scope_type=TimestampScopeType.ALL)
294302
self.sorted_blocks = MemoryTimestampIndex(scope_type=TimestampScopeType.BLOCKS)
@@ -304,6 +312,15 @@ def __init__(self) -> None:
304312
# XXX: this has to be at the end of __init__, after everything has been initialized
305313
self.__init_checks__()
306314

315+
def enable_tips_indexes(self) -> None:
316+
from hathor.indexes.memory_tips_index import MemoryTipsIndex
317+
if self.all_tips is None:
318+
self.all_tips = MemoryTipsIndex(scope_type=TipsScopeType.ALL)
319+
if self.block_tips is None:
320+
self.block_tips = MemoryTipsIndex(scope_type=TipsScopeType.BLOCKS)
321+
if self.tx_tips is None:
322+
self.tx_tips = MemoryTipsIndex(scope_type=TipsScopeType.TXS)
323+
307324
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
308325
from hathor.indexes.memory_address_index import MemoryAddressIndex
309326
if self.addresses is None:
@@ -332,7 +349,6 @@ def enable_deps_index(self) -> None:
332349

333350
class RocksDBIndexesManager(IndexesManager):
334351
def __init__(self, db: 'rocksdb.DB') -> None:
335-
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
336352
from hathor.indexes.rocksdb_height_index import RocksDBHeightIndex
337353
from hathor.indexes.rocksdb_info_index import RocksDBInfoIndex
338354
from hathor.indexes.rocksdb_timestamp_index import RocksDBTimestampIndex
@@ -341,9 +357,9 @@ def __init__(self, db: 'rocksdb.DB') -> None:
341357

342358
self.info = RocksDBInfoIndex(self._db)
343359
self.height = RocksDBHeightIndex(self._db)
344-
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL)
345-
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS)
346-
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS)
360+
self.all_tips = None
361+
self.block_tips = None
362+
self.tx_tips = None
347363

348364
self.sorted_all = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.ALL)
349365
self.sorted_blocks = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.BLOCKS)
@@ -358,6 +374,15 @@ def __init__(self, db: 'rocksdb.DB') -> None:
358374
# XXX: this has to be at the end of __init__, after everything has been initialized
359375
self.__init_checks__()
360376

377+
def enable_tips_indexes(self) -> None:
378+
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
379+
if self.all_tips is None:
380+
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL)
381+
if self.block_tips is None:
382+
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS)
383+
if self.tx_tips is None:
384+
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS)
385+
361386
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
362387
from hathor.indexes.rocksdb_address_index import RocksDBAddressIndex
363388
if self.addresses is None:

hathor/manager.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ def __init__(self,
167167

168168
self._event_manager = event_manager
169169

170+
if enable_sync_v1:
171+
assert self.tx_storage.indexes is not None
172+
self.log.debug('enable sync-v1 indexes')
173+
self.tx_storage.indexes.enable_tips_indexes()
170174
if enable_sync_v2:
171175
assert self.tx_storage.indexes is not None
172176
self.log.debug('enable sync-v2 indexes')
@@ -737,6 +741,42 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs':
737741
This method tries to return a stable result, such that for a given timestamp and storage state it will always
738742
return the same.
739743
"""
744+
# return self._generate_parent_txs_from_tips_index(timestamp)
745+
# XXX: prefer txs_tips index since it's been tested more
746+
assert self.tx_storage.indexes is not None
747+
if self.tx_storage.indexes.tx_tips is not None:
748+
return self._generate_parent_txs_from_tips_index(timestamp)
749+
else:
750+
return self._generate_parent_txs_from_mempool_index(timestamp)
751+
752+
def _generate_parent_txs_from_mempool_index(self, timestamp: Optional[float]) -> 'ParentTxs':
753+
# XXX: this implementation is naive, it will return a working result but not necessarily actual tips,
754+
# particularly when the timestamp is in the past it will just return tx parents of a previous block that
755+
# is within the timestamp, this is because we don't need to support that case for normal usage
756+
if timestamp is None:
757+
timestamp = self.reactor.seconds()
758+
assert self.tx_storage.indexes is not None
759+
assert self.tx_storage.indexes.height is not None
760+
assert self.tx_storage.indexes.mempool_tips is not None
761+
tips = [tx for tx in self.tx_storage.indexes.mempool_tips.iter(self.tx_storage) if tx.timestamp < timestamp]
762+
max_timestamp = max(tx.timestamp for tx in tips) if tips else 0
763+
can_include: List[bytes] = [not_none(tx.hash) for tx in tips]
764+
must_include = []
765+
if len(can_include) < 2:
766+
best_block = self.tx_storage.indexes.height.find_by_timestamp(timestamp, self.tx_storage)
767+
assert best_block is not None
768+
all_best_block_parent_txs = list(map(self.tx_storage.get_transaction, best_block.parents[1:]))
769+
best_block_parent_txs = [tx for tx in all_best_block_parent_txs if tx.timestamp < timestamp]
770+
max_timestamp = max(max_timestamp, *list(tx.timestamp for tx in best_block_parent_txs))
771+
if len(can_include) < 1:
772+
can_include.extend(not_none(tx.hash) for tx in best_block_parent_txs)
773+
else:
774+
must_include = can_include
775+
can_include = [not_none(tx.hash) for tx in best_block_parent_txs]
776+
assert len(can_include) + len(must_include) >= 2
777+
return ParentTxs(max_timestamp, can_include, must_include)
778+
779+
def _generate_parent_txs_from_tips_index(self, timestamp: Optional[float]) -> 'ParentTxs':
740780
if timestamp is None:
741781
timestamp = self.reactor.seconds()
742782
can_include_intervals = sorted(self.tx_storage.get_tx_tips(timestamp - 1))

hathor/transaction/storage/transaction_storage.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -528,15 +528,23 @@ def first_timestamp(self) -> int:
528528
raise NotImplementedError
529529

530530
@abstractmethod
531-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> List[bytes]:
531+
def get_best_block_tips(self, *, skip_cache: bool = False) -> List[bytes]:
532532
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
533533
534534
When more than one block is returned, it means that there are multiple best chains and
535535
you can choose any of them.
536536
"""
537-
if timestamp is None and not skip_cache and self._best_block_tips_cache is not None:
538-
return self._best_block_tips_cache[:]
537+
# ignoring cache because current implementation is ~O(1)
538+
assert self.indexes is not None
539+
return [self.indexes.height.get_tip()]
540+
541+
@abstractmethod
542+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> List[bytes]:
543+
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
539544
545+
When more than one block is returned, it means that there are multiple best chains and
546+
you can choose any of them.
547+
"""
540548
best_score = 0.0
541549
best_tip_blocks: List[bytes] = []
542550

@@ -915,6 +923,7 @@ def iter_mempool_tips_from_tx_tips(self) -> Iterator[Transaction]:
915923
This method requires indexes to be enabled.
916924
"""
917925
assert self.indexes is not None
926+
assert self.indexes.tx_tips is not None
918927
tx_tips = self.indexes.tx_tips
919928

920929
for interval in tx_tips[self.latest_timestamp + 1]:
@@ -1019,8 +1028,11 @@ def remove_cache(self) -> None:
10191028
"""Remove all caches in case we don't need it."""
10201029
self.indexes = None
10211030

1022-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> List[bytes]:
1023-
return super().get_best_block_tips(timestamp, skip_cache=skip_cache)
1031+
def get_best_block_tips(self, *, skip_cache: bool = False) -> List[bytes]:
1032+
return super().get_best_block_tips(skip_cache=skip_cache)
1033+
1034+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> List[bytes]:
1035+
return super().get_past_best_block_tips(timestamp)
10241036

10251037
def get_weight_best_block(self) -> float:
10261038
return super().get_weight_best_block()
@@ -1029,6 +1041,7 @@ def get_block_tips(self, timestamp: Optional[float] = None) -> Set[Interval]:
10291041
if self.indexes is None:
10301042
raise NotImplementedError
10311043
assert self.indexes is not None
1044+
assert self.indexes.block_tips is not None
10321045
if timestamp is None:
10331046
timestamp = self.latest_timestamp
10341047
return self.indexes.block_tips[timestamp]
@@ -1037,6 +1050,7 @@ def get_tx_tips(self, timestamp: Optional[float] = None) -> Set[Interval]:
10371050
if self.indexes is None:
10381051
raise NotImplementedError
10391052
assert self.indexes is not None
1053+
assert self.indexes.tx_tips is not None
10401054
if timestamp is None:
10411055
timestamp = self.latest_timestamp
10421056
tips = self.indexes.tx_tips[timestamp]
@@ -1054,6 +1068,7 @@ def get_all_tips(self, timestamp: Optional[float] = None) -> Set[Interval]:
10541068
if self.indexes is None:
10551069
raise NotImplementedError
10561070
assert self.indexes is not None
1071+
assert self.indexes.all_tips is not None
10571072
if timestamp is None:
10581073
timestamp = self.latest_timestamp
10591074

tests/p2p/test_double_spending.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ def test_simple_double_spending(self):
8484
self.assertEqual([tx1.hash, tx2.hash], spent_meta.spent_outputs[txin.index])
8585

8686
# old indexes
87-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
88-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
87+
if self.manager1.tx_storage.indexes.tx_tips is not None:
88+
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
89+
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
8990

9091
# new indexes
9192
if self.manager1.tx_storage.indexes.mempool_tips is not None:
@@ -114,9 +115,10 @@ def test_simple_double_spending(self):
114115
self.assertEqual([tx1.hash, tx2.hash, tx3.hash], spent_meta.spent_outputs[txin.index])
115116

116117
# old indexes
117-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
118-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
119-
self.assertIn(tx3.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
118+
if self.manager1.tx_storage.indexes.tx_tips is not None:
119+
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
120+
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
121+
self.assertIn(tx3.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
120122

121123
# new indexes
122124
if self.manager1.tx_storage.indexes.mempool_tips is not None:

0 commit comments

Comments
 (0)