Skip to content

Commit 4d7d22c

Browse files
committed
feat(indexes): make sync-v1 indexes optional
1 parent 947db7b commit 4d7d22c

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.types import VertexId
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,
@@ -87,6 +90,18 @@ def get(self, height: int) -> Optional[bytes]:
8790
"""
8891
raise NotImplementedError
8992

93+
def find_by_timestamp(self, timestamp: float, tx_storage: 'TransactionStorage') -> Optional[Block]:
94+
""" This method starts from the tip and advances to the parent until it finds a block with lower timestamp.
95+
"""
96+
# TODO: optimize
97+
if timestamp < self.get_genesis_block_entry().timestamp:
98+
return None
99+
block = tx_storage.get_transaction(self.get_tip())
100+
assert isinstance(block, Block)
101+
while block.timestamp > timestamp:
102+
block = block.get_block_parent()
103+
return block
104+
90105
@abstractmethod
91106
def get_tip(self) -> bytes:
92107
""" 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
@@ -51,14 +51,12 @@ class IndexesManager(ABC):
5151
log = get_logger()
5252

5353
info: InfoIndex
54-
all_tips: TipsIndex
55-
block_tips: TipsIndex
56-
tx_tips: TipsIndex
57-
54+
all_tips: Optional[TipsIndex]
55+
block_tips: Optional[TipsIndex]
56+
tx_tips: Optional[TipsIndex]
5857
sorted_all: TimestampIndex
5958
sorted_blocks: TimestampIndex
6059
sorted_txs: TimestampIndex
61-
6260
height: HeightIndex
6361
mempool_tips: Optional[MempoolTipsIndex]
6462
addresses: Optional[AddressIndex]
@@ -94,6 +92,11 @@ def iter_all_indexes(self) -> Iterator[BaseIndex]:
9492
self.utxo,
9593
])
9694

95+
@abstractmethod
96+
def enable_tips_indexes(self) -> None:
97+
"""Enable tips indexs. It does nothing if it has already been enabled."""
98+
raise NotImplementedError
99+
97100
@abstractmethod
98101
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
99102
"""Enable address index. It does nothing if it has already been enabled."""
@@ -198,18 +201,21 @@ def add_tx(self, tx: BaseTransaction) -> bool:
198201

199202
# These two calls return False when a transaction changes from
200203
# voided to executed and vice-versa.
201-
r1 = self.all_tips.add_tx(tx)
202-
r2 = self.sorted_all.add_tx(tx)
203-
assert r1 == r2
204+
r1 = self.sorted_all.add_tx(tx)
205+
if self.all_tips is not None:
206+
r2 = self.all_tips.add_tx(tx)
207+
assert r1 == r2
204208

205209
if tx.is_block:
206-
r3 = self.block_tips.add_tx(tx)
207-
r4 = self.sorted_blocks.add_tx(tx)
208-
assert r3 == r4
210+
r3 = self.sorted_blocks.add_tx(tx)
211+
if self.block_tips is not None:
212+
r4 = self.block_tips.add_tx(tx)
213+
assert r3 == r4
209214
else:
210-
r3 = self.tx_tips.add_tx(tx)
211-
r4 = self.sorted_txs.add_tx(tx)
212-
assert r3 == r4
215+
r3 = self.sorted_txs.add_tx(tx)
216+
if self.tx_tips is not None:
217+
r4 = self.tx_tips.add_tx(tx)
218+
assert r3 == r4
213219

214220
if self.addresses:
215221
self.addresses.add_tx(tx)
@@ -235,7 +241,8 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
235241
# We delete from indexes in two cases: (i) mark tx as voided, and (ii) remove tx.
236242
# We only remove tx from all_tips and sorted_all when it is removed from the storage.
237243
# For clarity, when a tx is marked as voided, it is not removed from all_tips and sorted_all.
238-
self.all_tips.del_tx(tx, relax_assert=relax_assert)
244+
if self.all_tips is not None:
245+
self.all_tips.del_tx(tx, relax_assert=relax_assert)
239246
self.sorted_all.del_tx(tx)
240247
if self.addresses:
241248
self.addresses.remove_tx(tx)
@@ -249,11 +256,13 @@ def del_tx(self, tx: BaseTransaction, *, remove_all: bool = False, relax_assert:
249256
self.mempool_tips.update(tx, remove=True)
250257

251258
if tx.is_block:
252-
self.block_tips.del_tx(tx, relax_assert=relax_assert)
253259
self.sorted_blocks.del_tx(tx)
260+
if self.block_tips is not None:
261+
self.block_tips.del_tx(tx, relax_assert=relax_assert)
254262
else:
255-
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
256263
self.sorted_txs.del_tx(tx)
264+
if self.tx_tips is not None:
265+
self.tx_tips.del_tx(tx, relax_assert=relax_assert)
257266

258267
if self.tokens:
259268
self.tokens.del_tx(tx)
@@ -264,12 +273,11 @@ def __init__(self) -> None:
264273
from hathor.indexes.memory_height_index import MemoryHeightIndex
265274
from hathor.indexes.memory_info_index import MemoryInfoIndex
266275
from hathor.indexes.memory_timestamp_index import MemoryTimestampIndex
267-
from hathor.indexes.memory_tips_index import MemoryTipsIndex
268276

269277
self.info = MemoryInfoIndex()
270-
self.all_tips = MemoryTipsIndex(scope_type=TipsScopeType.ALL)
271-
self.block_tips = MemoryTipsIndex(scope_type=TipsScopeType.BLOCKS)
272-
self.tx_tips = MemoryTipsIndex(scope_type=TipsScopeType.TXS)
278+
self.all_tips = None
279+
self.block_tips = None
280+
self.tx_tips = None
273281

274282
self.sorted_all = MemoryTimestampIndex(scope_type=TimestampScopeType.ALL)
275283
self.sorted_blocks = MemoryTimestampIndex(scope_type=TimestampScopeType.BLOCKS)
@@ -284,6 +292,15 @@ def __init__(self) -> None:
284292
# XXX: this has to be at the end of __init__, after everything has been initialized
285293
self.__init_checks__()
286294

295+
def enable_tips_indexes(self) -> None:
296+
from hathor.indexes.memory_tips_index import MemoryTipsIndex
297+
if self.all_tips is None:
298+
self.all_tips = MemoryTipsIndex(scope_type=TipsScopeType.ALL)
299+
if self.block_tips is None:
300+
self.block_tips = MemoryTipsIndex(scope_type=TipsScopeType.BLOCKS)
301+
if self.tx_tips is None:
302+
self.tx_tips = MemoryTipsIndex(scope_type=TipsScopeType.TXS)
303+
287304
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
288305
from hathor.indexes.memory_address_index import MemoryAddressIndex
289306
if self.addresses is None:
@@ -307,7 +324,6 @@ def enable_mempool_index(self) -> None:
307324

308325
class RocksDBIndexesManager(IndexesManager):
309326
def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
310-
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
311327
from hathor.indexes.rocksdb_height_index import RocksDBHeightIndex
312328
from hathor.indexes.rocksdb_info_index import RocksDBInfoIndex
313329
from hathor.indexes.rocksdb_timestamp_index import RocksDBTimestampIndex
@@ -316,9 +332,9 @@ def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
316332

317333
self.info = RocksDBInfoIndex(self._db)
318334
self.height = RocksDBHeightIndex(self._db)
319-
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL)
320-
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS)
321-
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS)
335+
self.all_tips = None
336+
self.block_tips = None
337+
self.tx_tips = None
322338

323339
self.sorted_all = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.ALL)
324340
self.sorted_blocks = RocksDBTimestampIndex(self._db, scope_type=TimestampScopeType.BLOCKS)
@@ -332,6 +348,15 @@ def __init__(self, rocksdb_storage: 'RocksDBStorage') -> None:
332348
# XXX: this has to be at the end of __init__, after everything has been initialized
333349
self.__init_checks__()
334350

351+
def enable_tips_indexes(self) -> None:
352+
from hathor.indexes.partial_rocksdb_tips_index import PartialRocksDBTipsIndex
353+
if self.all_tips is None:
354+
self.all_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.ALL)
355+
if self.block_tips is None:
356+
self.block_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.BLOCKS)
357+
if self.tx_tips is None:
358+
self.tx_tips = PartialRocksDBTipsIndex(self._db, scope_type=TipsScopeType.TXS)
359+
335360
def enable_address_index(self, pubsub: 'PubSubManager') -> None:
336361
from hathor.indexes.rocksdb_address_index import RocksDBAddressIndex
337362
if self.addresses is None:

hathor/manager.py

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

235235
self.manager = manager
236+
assert self.manager.tx_storage.indexes is not None
237+
indexes = self.manager.tx_storage.indexes
238+
if self.is_sync_version_available(SyncVersion.V1_1):
239+
self.log.debug('enable sync-v1 indexes')
240+
indexes.enable_tips_indexes()
236241
if self.is_sync_version_available(SyncVersion.V2):
237-
assert self.manager.tx_storage.indexes is not None
238-
indexes = self.manager.tx_storage.indexes
239242
self.log.debug('enable sync-v2 indexes')
240243
indexes.enable_mempool_index()
241244

hathor/transaction/storage/transaction_storage.py

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

649649
@abstractmethod
650-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
650+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
651651
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
652652
653653
When more than one block is returned, it means that there are multiple best chains and
654654
you can choose any of them.
655655
"""
656-
if timestamp is None and not skip_cache and self._best_block_tips_cache is not None:
657-
return self._best_block_tips_cache[:]
656+
# ignoring cache because current implementation is ~O(1)
657+
assert self.indexes is not None
658+
return [self.indexes.height.get_tip()]
659+
660+
@abstractmethod
661+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
662+
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
658663
664+
When more than one block is returned, it means that there are multiple best chains and
665+
you can choose any of them.
666+
"""
659667
best_score = 0.0
660668
best_tip_blocks: list[bytes] = []
661669

@@ -1050,6 +1058,7 @@ def iter_mempool_tips_from_tx_tips(self) -> Iterator[Transaction]:
10501058
This method requires indexes to be enabled.
10511059
"""
10521060
assert self.indexes is not None
1061+
assert self.indexes.tx_tips is not None
10531062
tx_tips = self.indexes.tx_tips
10541063

10551064
for interval in tx_tips[self.latest_timestamp + 1]:
@@ -1210,8 +1219,11 @@ def remove_cache(self) -> None:
12101219
"""Remove all caches in case we don't need it."""
12111220
self.indexes = None
12121221

1213-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
1214-
return super().get_best_block_tips(timestamp, skip_cache=skip_cache)
1222+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
1223+
return super().get_best_block_tips(skip_cache=skip_cache)
1224+
1225+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
1226+
return super().get_past_best_block_tips(timestamp)
12151227

12161228
def get_n_height_tips(self, n_blocks: int) -> list[HeightInfo]:
12171229
block = self.get_best_block()
@@ -1230,6 +1242,7 @@ def get_block_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
12301242
if self.indexes is None:
12311243
raise NotImplementedError
12321244
assert self.indexes is not None
1245+
assert self.indexes.block_tips is not None
12331246
if timestamp is None:
12341247
timestamp = self.latest_timestamp
12351248
return self.indexes.block_tips[timestamp]
@@ -1238,6 +1251,7 @@ def get_tx_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
12381251
if self.indexes is None:
12391252
raise NotImplementedError
12401253
assert self.indexes is not None
1254+
assert self.indexes.tx_tips is not None
12411255
if timestamp is None:
12421256
timestamp = self.latest_timestamp
12431257
tips = self.indexes.tx_tips[timestamp]
@@ -1255,6 +1269,7 @@ def get_all_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
12551269
if self.indexes is None:
12561270
raise NotImplementedError
12571271
assert self.indexes is not None
1272+
assert self.indexes.all_tips is not None
12581273
if timestamp is None:
12591274
timestamp = self.latest_timestamp
12601275

tests/p2p/test_double_spending.py

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

9191
# old indexes
92-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
93-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
92+
if self.manager1.tx_storage.indexes.tx_tips is not None:
93+
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
94+
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
9495

9596
# new indexes
9697
if self.manager1.tx_storage.indexes.mempool_tips is not None:
@@ -119,9 +120,10 @@ def test_simple_double_spending(self) -> None:
119120
self.assertEqual([tx1.hash, tx2.hash, tx3.hash], spent_meta.spent_outputs[txin.index])
120121

121122
# old indexes
122-
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
123-
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
124-
self.assertIn(tx3.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
123+
if self.manager1.tx_storage.indexes.tx_tips is not None:
124+
self.assertNotIn(tx1.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
125+
self.assertNotIn(tx2.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
126+
self.assertIn(tx3.hash, [x.data for x in self.manager1.tx_storage.get_tx_tips()])
125127

126128
# new indexes
127129
if self.manager1.tx_storage.indexes.mempool_tips is not None:

0 commit comments

Comments
 (0)