Skip to content

Commit c1a23d1

Browse files
committed
feat(indexes): make sync-v1 indexes optional
1 parent 3b02e6c commit c1a23d1

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,7 +13,7 @@
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
@@ -22,6 +22,9 @@
2222
from hathor.types import VertexId
2323
from hathor.util import not_none
2424

25+
if TYPE_CHECKING: # pragma: no cover
26+
from hathor.transaction.storage import TransactionStorage
27+
2528
SCOPE = Scope(
2629
include_blocks=True,
2730
include_txs=False,
@@ -84,6 +87,18 @@ def get(self, height: int) -> Optional[bytes]:
8487
"""
8588
raise NotImplementedError
8689

90+
def find_by_timestamp(self, timestamp: float, tx_storage: 'TransactionStorage') -> Optional[Block]:
91+
""" This method starts from the tip and advances to the parent until it finds a block with lower timestamp.
92+
"""
93+
# TODO: optimize
94+
if timestamp < BLOCK_GENESIS.timestamp:
95+
return None
96+
block = tx_storage.get_transaction(self.get_tip())
97+
assert isinstance(block, Block)
98+
while block.timestamp > timestamp:
99+
block = block.get_block_parent()
100+
return block
101+
87102
@abstractmethod
88103
def get_tip(self) -> bytes:
89104
""" 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
@@ -697,6 +697,42 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs':
697697
This method tries to return a stable result, such that for a given timestamp and storage state it will always
698698
return the same.
699699
"""
700+
# return self._generate_parent_txs_from_tips_index(timestamp)
701+
# XXX: prefer txs_tips index since it's been tested more
702+
assert self.tx_storage.indexes is not None
703+
if self.tx_storage.indexes.tx_tips is not None:
704+
return self._generate_parent_txs_from_tips_index(timestamp)
705+
else:
706+
return self._generate_parent_txs_from_mempool_index(timestamp)
707+
708+
def _generate_parent_txs_from_mempool_index(self, timestamp: Optional[float]) -> 'ParentTxs':
709+
# XXX: this implementation is naive, it will return a working result but not necessarily actual tips,
710+
# particularly when the timestamp is in the past it will just return tx parents of a previous block that
711+
# is within the timestamp, this is because we don't need to support that case for normal usage
712+
if timestamp is None:
713+
timestamp = self.reactor.seconds()
714+
assert self.tx_storage.indexes is not None
715+
assert self.tx_storage.indexes.height is not None
716+
assert self.tx_storage.indexes.mempool_tips is not None
717+
tips = [tx for tx in self.tx_storage.indexes.mempool_tips.iter(self.tx_storage) if tx.timestamp < timestamp]
718+
max_timestamp = max(tx.timestamp for tx in tips) if tips else 0
719+
can_include: list[bytes] = [not_none(tx.hash) for tx in tips]
720+
must_include = []
721+
if len(can_include) < 2:
722+
best_block = self.tx_storage.indexes.height.find_by_timestamp(timestamp, self.tx_storage)
723+
assert best_block is not None
724+
all_best_block_parent_txs = list(map(self.tx_storage.get_transaction, best_block.parents[1:]))
725+
best_block_parent_txs = [tx for tx in all_best_block_parent_txs if tx.timestamp < timestamp]
726+
max_timestamp = max(max_timestamp, *list(tx.timestamp for tx in best_block_parent_txs))
727+
if len(can_include) < 1:
728+
can_include.extend(not_none(tx.hash) for tx in best_block_parent_txs)
729+
else:
730+
must_include = can_include
731+
can_include = [not_none(tx.hash) for tx in best_block_parent_txs]
732+
assert len(can_include) + len(must_include) >= 2
733+
return ParentTxs(max_timestamp, can_include, must_include)
734+
735+
def _generate_parent_txs_from_tips_index(self, timestamp: Optional[float]) -> 'ParentTxs':
700736
if timestamp is None:
701737
timestamp = self.reactor.seconds()
702738
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
@@ -625,15 +625,23 @@ def first_timestamp(self) -> int:
625625
raise NotImplementedError
626626

627627
@abstractmethod
628-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
628+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
629629
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
630630
631631
When more than one block is returned, it means that there are multiple best chains and
632632
you can choose any of them.
633633
"""
634-
if timestamp is None and not skip_cache and self._best_block_tips_cache is not None:
635-
return self._best_block_tips_cache[:]
634+
# ignoring cache because current implementation is ~O(1)
635+
assert self.indexes is not None
636+
return [self.indexes.height.get_tip()]
637+
638+
@abstractmethod
639+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
640+
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
636641
642+
When more than one block is returned, it means that there are multiple best chains and
643+
you can choose any of them.
644+
"""
637645
best_score = 0.0
638646
best_tip_blocks: list[bytes] = []
639647

@@ -1019,6 +1027,7 @@ def iter_mempool_tips_from_tx_tips(self) -> Iterator[Transaction]:
10191027
This method requires indexes to be enabled.
10201028
"""
10211029
assert self.indexes is not None
1030+
assert self.indexes.tx_tips is not None
10221031
tx_tips = self.indexes.tx_tips
10231032

10241033
for interval in tx_tips[self.latest_timestamp + 1]:
@@ -1121,8 +1130,11 @@ def remove_cache(self) -> None:
11211130
"""Remove all caches in case we don't need it."""
11221131
self.indexes = None
11231132

1124-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
1125-
return super().get_best_block_tips(timestamp, skip_cache=skip_cache)
1133+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
1134+
return super().get_best_block_tips(skip_cache=skip_cache)
1135+
1136+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
1137+
return super().get_past_best_block_tips(timestamp)
11261138

11271139
def get_n_height_tips(self, n_blocks: int) -> list[HeightInfo]:
11281140
block = self.get_best_block()
@@ -1141,6 +1153,7 @@ def get_block_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11411153
if self.indexes is None:
11421154
raise NotImplementedError
11431155
assert self.indexes is not None
1156+
assert self.indexes.block_tips is not None
11441157
if timestamp is None:
11451158
timestamp = self.latest_timestamp
11461159
return self.indexes.block_tips[timestamp]
@@ -1149,6 +1162,7 @@ def get_tx_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11491162
if self.indexes is None:
11501163
raise NotImplementedError
11511164
assert self.indexes is not None
1165+
assert self.indexes.tx_tips is not None
11521166
if timestamp is None:
11531167
timestamp = self.latest_timestamp
11541168
tips = self.indexes.tx_tips[timestamp]
@@ -1166,6 +1180,7 @@ def get_all_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11661180
if self.indexes is None:
11671181
raise NotImplementedError
11681182
assert self.indexes is not None
1183+
assert self.indexes.all_tips is not None
11691184
if timestamp is None:
11701185
timestamp = self.latest_timestamp
11711186

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)