Skip to content

Commit d26a92b

Browse files
committed
feat(indexes): make sync-v1 indexes optional
1 parent 405ab6a commit d26a92b

File tree

10 files changed

+218
-88
lines changed

10 files changed

+218
-88
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 NamedTuple, Optional
16+
from typing import TYPE_CHECKING, NamedTuple, Optional
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,
@@ -77,6 +80,18 @@ def get(self, height: int) -> Optional[bytes]:
7780
"""
7881
raise NotImplementedError
7982

83+
def find_by_timestamp(self, timestamp: float, tx_storage: 'TransactionStorage') -> Optional[Block]:
84+
""" This method starts from the tip and advances to the parent until it finds a block with lower timestamp.
85+
"""
86+
# TODO: optimize
87+
if timestamp < BLOCK_GENESIS.timestamp:
88+
return None
89+
block = tx_storage.get_transaction(self.get_tip())
90+
assert isinstance(block, Block)
91+
while block.timestamp > timestamp:
92+
block = block.get_block_parent()
93+
return block
94+
8095
@abstractmethod
8196
def get_tip(self) -> bytes:
8297
""" 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
@@ -52,14 +52,12 @@ class IndexesManager(ABC):
5252
log = get_logger()
5353

5454
info: InfoIndex
55-
all_tips: TipsIndex
56-
block_tips: TipsIndex
57-
tx_tips: TipsIndex
58-
55+
all_tips: Optional[TipsIndex]
56+
block_tips: Optional[TipsIndex]
57+
tx_tips: Optional[TipsIndex]
5958
sorted_all: TimestampIndex
6059
sorted_blocks: TimestampIndex
6160
sorted_txs: TimestampIndex
62-
6361
height: HeightIndex
6462
deps: Optional[DepsIndex]
6563
mempool_tips: Optional[MempoolTipsIndex]
@@ -97,6 +95,11 @@ def iter_all_indexes(self) -> Iterator[BaseIndex]:
9795
self.utxo,
9896
])
9997

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

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

215219
if tx.is_block:
216-
r3 = self.block_tips.add_tx(tx)
217-
r4 = self.sorted_blocks.add_tx(tx)
218-
assert r3 == r4
220+
r3 = self.sorted_blocks.add_tx(tx)
221+
if self.block_tips is not None:
222+
r4 = self.block_tips.add_tx(tx)
223+
assert r3 == r4
219224
else:
220-
r3 = self.tx_tips.add_tx(tx)
221-
r4 = self.sorted_txs.add_tx(tx)
222-
assert r3 == r4
225+
r3 = self.sorted_txs.add_tx(tx)
226+
if self.tx_tips is not None:
227+
r4 = self.tx_tips.add_tx(tx)
228+
assert r3 == r4
223229

224230
if self.addresses:
225231
self.addresses.add_tx(tx)
@@ -249,7 +255,8 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
249255
# We delete from indexes in two cases: (i) mark tx as voided, and (ii) remove tx.
250256
# We only remove tx from all_tips and sorted_all when it is removed from the storage.
251257
# For clarity, when a tx is marked as voided, it is not removed from all_tips and sorted_all.
252-
self.all_tips.del_tx(tx, relax_assert=relax_assert)
258+
if self.all_tips is not None:
259+
self.all_tips.del_tx(tx, relax_assert=relax_assert)
253260
self.sorted_all.del_tx(tx)
254261
if self.addresses:
255262
self.addresses.remove_tx(tx)
@@ -263,11 +270,13 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
263270
self.mempool_tips.update(tx, remove=True)
264271

265272
if tx.is_block:
266-
self.block_tips.del_tx(tx, relax_assert=relax_assert)
267273
self.sorted_blocks.del_tx(tx)
274+
if self.block_tips is not None:
275+
self.block_tips.del_tx(tx, relax_assert=relax_assert)
268276
else:
269-
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
270277
self.sorted_txs.del_tx(tx)
278+
if self.tx_tips is not None:
279+
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
271280

272281
if self.tokens:
273282
self.tokens.del_tx(tx)
@@ -282,12 +291,11 @@ def __init__(self) -> None:
282291
from hathor.indexes.memory_height_index import MemoryHeightIndex
283292
from hathor.indexes.memory_info_index import MemoryInfoIndex
284293
from hathor.indexes.memory_timestamp_index import MemoryTimestampIndex
285-
from hathor.indexes.memory_tips_index import MemoryTipsIndex
286294

287295
self.info = MemoryInfoIndex()
288-
self.all_tips = MemoryTipsIndex(scope_type=TipsScopeType.ALL)
289-
self.block_tips = MemoryTipsIndex(scope_type=TipsScopeType.BLOCKS)
290-
self.tx_tips = MemoryTipsIndex(scope_type=TipsScopeType.TXS)
296+
self.all_tips = None
297+
self.block_tips = None
298+
self.tx_tips = None
291299

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

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

332349
class RocksDBIndexesManager(IndexesManager):
333350
def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
334-
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
335351
from hathor.indexes.rocksdb_height_index import RocksDBHeightIndex
336352
from hathor.indexes.rocksdb_info_index import RocksDBInfoIndex
337353
from hathor.indexes.rocksdb_timestamp_index import RocksDBTimestampIndex
@@ -340,9 +356,9 @@ def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
340356

341357
self.info = RocksDBInfoIndex(self._db)
342358
self.height = RocksDBHeightIndex(self._db)
343-
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL)
344-
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS)
345-
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS)
359+
self.all_tips = None
360+
self.block_tips = None
361+
self.tx_tips = None
346362

347363
self.sorted_all = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.ALL)
348364
self.sorted_blocks = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.BLOCKS)
@@ -357,6 +373,15 @@ def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
357373
# XXX: this has to be at the end of __init__, after everything has been initialized
358374
self.__init_checks__()
359375

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

hathor/manager.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,42 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs':
693693
This method tries to return a stable result, such that for a given timestamp and storage state it will always
694694
return the same.
695695
"""
696+
# return self._generate_parent_txs_from_tips_index(timestamp)
697+
# XXX: prefer txs_tips index since it's been tested more
698+
assert self.tx_storage.indexes is not None
699+
if self.tx_storage.indexes.tx_tips is not None:
700+
return self._generate_parent_txs_from_tips_index(timestamp)
701+
else:
702+
return self._generate_parent_txs_from_mempool_index(timestamp)
703+
704+
def _generate_parent_txs_from_mempool_index(self, timestamp: Optional[float]) -> 'ParentTxs':
705+
# XXX: this implementation is naive, it will return a working result but not necessarily actual tips,
706+
# particularly when the timestamp is in the past it will just return tx parents of a previous block that
707+
# is within the timestamp, this is because we don't need to support that case for normal usage
708+
if timestamp is None:
709+
timestamp = self.reactor.seconds()
710+
assert self.tx_storage.indexes is not None
711+
assert self.tx_storage.indexes.height is not None
712+
assert self.tx_storage.indexes.mempool_tips is not None
713+
tips = [tx for tx in self.tx_storage.indexes.mempool_tips.iter(self.tx_storage) if tx.timestamp < timestamp]
714+
max_timestamp = max(tx.timestamp for tx in tips) if tips else 0
715+
can_include: list[bytes] = [not_none(tx.hash) for tx in tips]
716+
must_include = []
717+
if len(can_include) < 2:
718+
best_block = self.tx_storage.indexes.height.find_by_timestamp(timestamp, self.tx_storage)
719+
assert best_block is not None
720+
all_best_block_parent_txs = list(map(self.tx_storage.get_transaction, best_block.parents[1:]))
721+
best_block_parent_txs = [tx for tx in all_best_block_parent_txs if tx.timestamp < timestamp]
722+
max_timestamp = max(max_timestamp, *list(tx.timestamp for tx in best_block_parent_txs))
723+
if len(can_include) < 1:
724+
can_include.extend(not_none(tx.hash) for tx in best_block_parent_txs)
725+
else:
726+
must_include = can_include
727+
can_include = [not_none(tx.hash) for tx in best_block_parent_txs]
728+
assert len(can_include) + len(must_include) >= 2
729+
return ParentTxs(max_timestamp, can_include, must_include)
730+
731+
def _generate_parent_txs_from_tips_index(self, timestamp: Optional[float]) -> 'ParentTxs':
696732
if timestamp is None:
697733
timestamp = self.reactor.seconds()
698734
can_include_intervals = sorted(self.tx_storage.get_tx_tips(timestamp - 1))

hathor/p2p/manager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,12 @@ def __init__(self,
191191
def set_manager(self, manager: 'HathorManager') -> None:
192192
"""Set the manager. This method must be called before start()."""
193193
self.manager = manager
194+
assert self.manager.tx_storage.indexes is not None
195+
indexes = self.manager.tx_storage.indexes
196+
if self.enable_sync_v1 or self.enable_sync_v1_1:
197+
self.log.debug('enable sync-v1 indexes')
198+
indexes.enable_tips_indexes()
194199
if self.enable_sync_v2:
195-
assert self.manager.tx_storage.indexes is not None
196-
indexes = self.manager.tx_storage.indexes
197200
self.log.debug('enable sync-v2 indexes')
198201
indexes.enable_deps_index()
199202
indexes.enable_mempool_index()

hathor/transaction/storage/transaction_storage.py

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

624624
@abstractmethod
625-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
625+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
626626
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
627627
628628
When more than one block is returned, it means that there are multiple best chains and
629629
you can choose any of them.
630630
"""
631-
if timestamp is None and not skip_cache and self._best_block_tips_cache is not None:
632-
return self._best_block_tips_cache[:]
631+
# ignoring cache because current implementation is ~O(1)
632+
assert self.indexes is not None
633+
return [self.indexes.height.get_tip()]
634+
635+
@abstractmethod
636+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
637+
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
633638
639+
When more than one block is returned, it means that there are multiple best chains and
640+
you can choose any of them.
641+
"""
634642
best_score = 0.0
635643
best_tip_blocks: list[bytes] = []
636644

@@ -1011,6 +1019,7 @@ def iter_mempool_tips_from_tx_tips(self) -> Iterator[Transaction]:
10111019
This method requires indexes to be enabled.
10121020
"""
10131021
assert self.indexes is not None
1022+
assert self.indexes.tx_tips is not None
10141023
tx_tips = self.indexes.tx_tips
10151024

10161025
for interval in tx_tips[self.latest_timestamp + 1]:
@@ -1111,8 +1120,11 @@ def remove_cache(self) -> None:
11111120
"""Remove all caches in case we don't need it."""
11121121
self.indexes = None
11131122

1114-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
1115-
return super().get_best_block_tips(timestamp, skip_cache=skip_cache)
1123+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
1124+
return super().get_best_block_tips(skip_cache=skip_cache)
1125+
1126+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
1127+
return super().get_past_best_block_tips(timestamp)
11161128

11171129
def get_weight_best_block(self) -> float:
11181130
return super().get_weight_best_block()
@@ -1121,6 +1133,7 @@ def get_block_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11211133
if self.indexes is None:
11221134
raise NotImplementedError
11231135
assert self.indexes is not None
1136+
assert self.indexes.block_tips is not None
11241137
if timestamp is None:
11251138
timestamp = self.latest_timestamp
11261139
return self.indexes.block_tips[timestamp]
@@ -1129,6 +1142,7 @@ def get_tx_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11291142
if self.indexes is None:
11301143
raise NotImplementedError
11311144
assert self.indexes is not None
1145+
assert self.indexes.tx_tips is not None
11321146
if timestamp is None:
11331147
timestamp = self.latest_timestamp
11341148
tips = self.indexes.tx_tips[timestamp]
@@ -1146,6 +1160,7 @@ def get_all_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11461160
if self.indexes is None:
11471161
raise NotImplementedError
11481162
assert self.indexes is not None
1163+
assert self.indexes.all_tips is not None
11491164
if timestamp is None:
11501165
timestamp = self.latest_timestamp
11511166

tests/p2p/test_double_spending.py

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

8484
# old indexes
85-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
86-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
85+
if self.manager1.tx_storage.indexes.tx_tips is not None:
86+
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
87+
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
8788

8889
# new indexes
8990
if self.manager1.tx_storage.indexes.mempool_tips is not None:
@@ -112,9 +113,10 @@ def test_simple_double_spending(self):
112113
self.assertEqual([tx1.hash, tx2.hash, tx3.hash], spent_meta.spent_outputs[txin.index])
113114

114115
# old indexes
115-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
116-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
117-
self.assertIn(tx3.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
116+
if self.manager1.tx_storage.indexes.tx_tips is not None:
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()])
118120

119121
# new indexes
120122
if self.manager1.tx_storage.indexes.mempool_tips is not None:

0 commit comments

Comments
 (0)