Skip to content

Commit 603e8cd

Browse files
committed
feat(indexes): make sync-v1 indexes optional
1 parent 29a622f commit 603e8cd

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 < BLOCK_GENESIS.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
@@ -673,6 +673,42 @@ def generate_parent_txs(self, timestamp: Optional[float]) -> 'ParentTxs':
673673
This method tries to return a stable result, such that for a given timestamp and storage state it will always
674674
return the same.
675675
"""
676+
# return self._generate_parent_txs_from_tips_index(timestamp)
677+
# XXX: prefer txs_tips index since it's been tested more
678+
assert self.tx_storage.indexes is not None
679+
if self.tx_storage.indexes.tx_tips is not None:
680+
return self._generate_parent_txs_from_tips_index(timestamp)
681+
else:
682+
return self._generate_parent_txs_from_mempool_index(timestamp)
683+
684+
def _generate_parent_txs_from_mempool_index(self, timestamp: Optional[float]) -> 'ParentTxs':
685+
# XXX: this implementation is naive, it will return a working result but not necessarily actual tips,
686+
# particularly when the timestamp is in the past it will just return tx parents of a previous block that
687+
# is within the timestamp, this is because we don't need to support that case for normal usage
688+
if timestamp is None:
689+
timestamp = self.reactor.seconds()
690+
assert self.tx_storage.indexes is not None
691+
assert self.tx_storage.indexes.height is not None
692+
assert self.tx_storage.indexes.mempool_tips is not None
693+
tips = [tx for tx in self.tx_storage.indexes.mempool_tips.iter(self.tx_storage) if tx.timestamp < timestamp]
694+
max_timestamp = max(tx.timestamp for tx in tips) if tips else 0
695+
can_include: list[bytes] = [not_none(tx.hash) for tx in tips]
696+
must_include = []
697+
if len(can_include) < 2:
698+
best_block = self.tx_storage.indexes.height.find_by_timestamp(timestamp, self.tx_storage)
699+
assert best_block is not None
700+
all_best_block_parent_txs = list(map(self.tx_storage.get_transaction, best_block.parents[1:]))
701+
best_block_parent_txs = [tx for tx in all_best_block_parent_txs if tx.timestamp < timestamp]
702+
max_timestamp = max(max_timestamp, *list(tx.timestamp for tx in best_block_parent_txs))
703+
if len(can_include) < 1:
704+
can_include.extend(not_none(tx.hash) for tx in best_block_parent_txs)
705+
else:
706+
must_include = can_include
707+
can_include = [not_none(tx.hash) for tx in best_block_parent_txs]
708+
assert len(can_include) + len(must_include) >= 2
709+
return ParentTxs(max_timestamp, can_include, must_include)
710+
711+
def _generate_parent_txs_from_tips_index(self, timestamp: Optional[float]) -> 'ParentTxs':
676712
if timestamp is None:
677713
timestamp = self.reactor.seconds()
678714
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
@@ -229,9 +229,12 @@ def set_manager(self, manager: 'HathorManager') -> None:
229229
raise TypeError('Class built incorrectly without any enabled sync version')
230230

231231
self.manager = manager
232+
assert self.manager.tx_storage.indexes is not None
233+
indexes = self.manager.tx_storage.indexes
234+
if self.is_sync_version_available(SyncVersion.V1_1):
235+
self.log.debug('enable sync-v1 indexes')
236+
indexes.enable_tips_indexes()
232237
if self.is_sync_version_available(SyncVersion.V2):
233-
assert self.manager.tx_storage.indexes is not None
234-
indexes = self.manager.tx_storage.indexes
235238
self.log.debug('enable sync-v2 indexes')
236239
indexes.enable_mempool_index()
237240

hathor/transaction/storage/transaction_storage.py

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

637637
@abstractmethod
638-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
638+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
639639
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
640640
641641
When more than one block is returned, it means that there are multiple best chains and
642642
you can choose any of them.
643643
"""
644-
if timestamp is None and not skip_cache and self._best_block_tips_cache is not None:
645-
return self._best_block_tips_cache[:]
644+
# ignoring cache because current implementation is ~O(1)
645+
assert self.indexes is not None
646+
return [self.indexes.height.get_tip()]
647+
648+
@abstractmethod
649+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
650+
""" Return a list of blocks that are heads in a best chain. It must be used when mining.
646651
652+
When more than one block is returned, it means that there are multiple best chains and
653+
you can choose any of them.
654+
"""
647655
best_score = 0.0
648656
best_tip_blocks: list[bytes] = []
649657

@@ -1029,6 +1037,7 @@ def iter_mempool_tips_from_tx_tips(self) -> Iterator[Transaction]:
10291037
This method requires indexes to be enabled.
10301038
"""
10311039
assert self.indexes is not None
1040+
assert self.indexes.tx_tips is not None
10321041
tx_tips = self.indexes.tx_tips
10331042

10341043
for interval in tx_tips[self.latest_timestamp + 1]:
@@ -1178,8 +1187,11 @@ def remove_cache(self) -> None:
11781187
"""Remove all caches in case we don't need it."""
11791188
self.indexes = None
11801189

1181-
def get_best_block_tips(self, timestamp: Optional[float] = None, *, skip_cache: bool = False) -> list[bytes]:
1182-
return super().get_best_block_tips(timestamp, skip_cache=skip_cache)
1190+
def get_best_block_tips(self, *, skip_cache: bool = False) -> list[bytes]:
1191+
return super().get_best_block_tips(skip_cache=skip_cache)
1192+
1193+
def get_past_best_block_tips(self, timestamp: Optional[float] = None) -> list[bytes]:
1194+
return super().get_past_best_block_tips(timestamp)
11831195

11841196
def get_n_height_tips(self, n_blocks: int) -> list[HeightInfo]:
11851197
block = self.get_best_block()
@@ -1198,6 +1210,7 @@ def get_block_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
11981210
if self.indexes is None:
11991211
raise NotImplementedError
12001212
assert self.indexes is not None
1213+
assert self.indexes.block_tips is not None
12011214
if timestamp is None:
12021215
timestamp = self.latest_timestamp
12031216
return self.indexes.block_tips[timestamp]
@@ -1206,6 +1219,7 @@ def get_tx_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
12061219
if self.indexes is None:
12071220
raise NotImplementedError
12081221
assert self.indexes is not None
1222+
assert self.indexes.tx_tips is not None
12091223
if timestamp is None:
12101224
timestamp = self.latest_timestamp
12111225
tips = self.indexes.tx_tips[timestamp]
@@ -1223,6 +1237,7 @@ def get_all_tips(self, timestamp: Optional[float] = None) -> set[Interval]:
12231237
if self.indexes is None:
12241238
raise NotImplementedError
12251239
assert self.indexes is not None
1240+
assert self.indexes.all_tips is not None
12261241
if timestamp is None:
12271242
timestamp = self.latest_timestamp
12281243

tests/p2p/test_double_spending.py

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

8585
# old indexes
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()])
86+
if self.manager1.tx_storage.indexes.tx_tips is not None:
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()])
8889

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

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

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

0 commit comments

Comments
 (0)