Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 053f8c6

Browse files
committed
Add persist_unexecuted_block API to ChainDB
1 parent a876368 commit 053f8c6

File tree

5 files changed

+180
-17
lines changed

5 files changed

+180
-17
lines changed

eth/abc.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,10 +631,35 @@ def persist_block(self,
631631
as genesis. Providing a ``genesis_parent_hash`` allows storage of blocks that
632632
aren't (yet) connected back to the true genesis header.
633633
634-
Assumes all block transactions have been persisted already.
634+
.. warning::
635+
This API assumes all block transactions have been persisted already. Use
636+
:meth:`eth.abc.ChainDatabaseAPI.persist_unexecuted_block` to persist blocks that were
637+
not executed.
635638
"""
636639
...
637640

641+
@abstractmethod
642+
def persist_unexecuted_block(self,
643+
block: BlockAPI,
644+
receipts: Tuple[ReceiptAPI, ...],
645+
genesis_parent_hash: Hash32 = None
646+
) -> Tuple[Tuple[Hash32, ...], Tuple[Hash32, ...]]:
647+
"""
648+
Persist the given block's header, uncles, transactions, and receipts. Does
649+
**not** validate if state transitions are valid.
650+
651+
:param block: the block that gets persisted
652+
:param receipts: the receipts for the given block
653+
:param genesis_parent_hash: *optional* parent hash of the header that is treated
654+
as genesis. Providing a ``genesis_parent_hash`` allows storage of blocks that
655+
aren't (yet) connected back to the true genesis header.
656+
657+
This API should be used to persist blocks that the EVM does not execute but which it
658+
stores to make them available. It ensures to persist receipts and transactions which
659+
:meth:`eth.abc.ChainDatabaseAPI.persist_block` in contrast assumes to be persisted
660+
separately.
661+
"""
662+
638663
@abstractmethod
639664
def persist_uncles(self, uncles: Tuple[BlockHeaderAPI]) -> Hash32:
640665
"""

eth/db/chain.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
EMPTY_UNCLE_HASH,
3535
GENESIS_PARENT_HASH,
3636
)
37+
from eth.db.trie import make_trie_root_and_nodes
3738
from eth.exceptions import (
3839
HeaderNotFound,
3940
ReceiptNotFound,
@@ -129,6 +130,34 @@ def persist_block(self,
129130
with self.db.atomic_batch() as db:
130131
return self._persist_block(db, block, genesis_parent_hash)
131132

133+
def persist_unexecuted_block(self,
134+
block: BlockAPI,
135+
receipts: Tuple[ReceiptAPI, ...],
136+
genesis_parent_hash: Hash32 = GENESIS_PARENT_HASH
137+
) -> Tuple[Tuple[Hash32, ...], Tuple[Hash32, ...]]:
138+
139+
tx_root_hash, tx_kv_nodes = make_trie_root_and_nodes(block.transactions)
140+
141+
if tx_root_hash != block.header.transaction_root:
142+
raise ValidationError(
143+
f"Block's transaction_root ({block.header.transaction_root}) "
144+
f"does not match expected value: {tx_root_hash}"
145+
)
146+
147+
receipt_root_hash, receipt_kv_nodes = make_trie_root_and_nodes(receipts)
148+
149+
if receipt_root_hash != block.header.receipt_root:
150+
raise ValidationError(
151+
f"Block's receipt_root ({block.header.receipt_root}) "
152+
f"does not match expected value: {receipt_root_hash}"
153+
)
154+
155+
with self.db.atomic_batch() as db:
156+
self._persist_trie_data_dict(db, receipt_kv_nodes)
157+
self._persist_trie_data_dict(db, tx_kv_nodes)
158+
159+
return self._persist_block(db, block, genesis_parent_hash)
160+
132161
@classmethod
133162
def _persist_block(
134163
cls,
@@ -347,6 +376,9 @@ def get(self, key: bytes) -> bytes:
347376
return self.db[key]
348377

349378
def persist_trie_data_dict(self, trie_data_dict: Dict[Hash32, bytes]) -> None:
350-
with self.db.atomic_batch() as db:
351-
for key, value in trie_data_dict.items():
352-
db[key] = value
379+
self._persist_trie_data_dict(self.db, trie_data_dict)
380+
381+
@classmethod
382+
def _persist_trie_data_dict(cls, db: DatabaseAPI, trie_data_dict: Dict[Hash32, bytes]) -> None:
383+
for key, value in trie_data_dict.items():
384+
db[key] = value

newsfragments/1925.feature.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add a new ``persist_unexecuted_block`` API to ``ChainDB``. This API should be used to persist
2+
a block without executing the EVM on it. The API is used by
3+
syncing strategies that do not execute all blocks but fill old blocks
4+
back in (e.g. ``beam`` or ``fast`` sync)

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ def chain_without_block_validation(
242242
return _chain_without_block_validation(request, VM, base_db, genesis_state)
243243

244244

245+
@pytest.fixture(params=[Chain, MiningChain])
246+
def chain_without_block_validation_factory(request, VM, genesis_state):
247+
return lambda db: _chain_without_block_validation(request, VM, db, genesis_state)
248+
249+
245250
@pytest.fixture(params=[Chain, MiningChain])
246251
def chain_without_block_validation_from_vm(request, base_db, genesis_state):
247252
"""

tests/database/test_eth1_chaindb.py

Lines changed: 110 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from eth.chains.base import (
1616
MiningChain,
1717
)
18+
from eth.db.atomic import AtomicDB
1819
from eth.db.chain import (
1920
ChainDB,
2021
)
@@ -149,30 +150,45 @@ def test_chaindb_get_canonical_block_hash(chaindb, block):
149150
assert block_hash == block.hash
150151

151152

152-
def test_chaindb_get_receipt_by_index(
153-
chain,
154-
funded_address,
155-
funded_address_private_key):
156-
NUMBER_BLOCKS_IN_CHAIN = 5
157-
TRANSACTIONS_IN_BLOCK = 10
158-
REQUIRED_BLOCK_NUMBER = 2
159-
REQUIRED_RECEIPT_INDEX = 3
153+
def mine_blocks_with_receipts(chain,
154+
num_blocks,
155+
num_tx_per_block,
156+
funded_address,
157+
funded_address_private_key):
160158

161-
for block_number in range(NUMBER_BLOCKS_IN_CHAIN):
162-
for tx_index in range(TRANSACTIONS_IN_BLOCK):
159+
for _ in range(num_blocks):
160+
block_receipts = []
161+
for _ in range(num_tx_per_block):
163162
tx = new_transaction(
164163
chain.get_vm(),
165164
from_=funded_address,
166165
to=force_bytes_to_address(b'\x10\x10'),
167166
private_key=funded_address_private_key,
168167
)
169168
new_block, tx_receipt, computation = chain.apply_transaction(tx)
169+
block_receipts.append(tx_receipt)
170170
computation.raise_if_error()
171171

172-
if (block_number + 1) == REQUIRED_BLOCK_NUMBER and tx_index == REQUIRED_RECEIPT_INDEX:
173-
actual_receipt = tx_receipt
172+
yield chain.mine_block(), block_receipts
173+
174174

175-
chain.mine_block()
175+
def test_chaindb_get_receipt_and_tx_by_index(chain, funded_address, funded_address_private_key):
176+
NUMBER_BLOCKS_IN_CHAIN = 5
177+
TRANSACTIONS_IN_BLOCK = 10
178+
REQUIRED_BLOCK_NUMBER = 2
179+
REQUIRED_RECEIPT_INDEX = 3
180+
181+
for (block, receipts) in mine_blocks_with_receipts(
182+
chain,
183+
NUMBER_BLOCKS_IN_CHAIN,
184+
TRANSACTIONS_IN_BLOCK,
185+
funded_address,
186+
funded_address_private_key,
187+
):
188+
if block.header.block_number == REQUIRED_BLOCK_NUMBER:
189+
actual_receipt = receipts[REQUIRED_RECEIPT_INDEX]
190+
actual_tx = block.transactions[REQUIRED_RECEIPT_INDEX]
191+
tx_class = block.transaction_class
176192

177193
# Check that the receipt retrieved is indeed the actual one
178194
chaindb_retrieved_receipt = chain.chaindb.get_receipt_by_index(
@@ -181,6 +197,10 @@ def test_chaindb_get_receipt_by_index(
181197
)
182198
assert chaindb_retrieved_receipt == actual_receipt
183199

200+
chaindb_retrieved_tx = chain.chaindb.get_transaction_by_index(
201+
REQUIRED_BLOCK_NUMBER, REQUIRED_RECEIPT_INDEX, tx_class)
202+
assert chaindb_retrieved_tx == actual_tx
203+
184204
# Raise error if block number is not found
185205
with pytest.raises(ReceiptNotFound):
186206
chain.chaindb.get_receipt_by_index(
@@ -194,3 +214,80 @@ def test_chaindb_get_receipt_by_index(
194214
NUMBER_BLOCKS_IN_CHAIN,
195215
TRANSACTIONS_IN_BLOCK + 1,
196216
)
217+
218+
219+
@pytest.mark.parametrize(
220+
"use_persist_unexecuted_block",
221+
(
222+
True,
223+
pytest.param(
224+
False,
225+
marks=pytest.mark.xfail(
226+
reason=(
227+
"The `persist_block` API relies on block execution to persist"
228+
"transactions and receipts. It is expected to fail this test."
229+
)
230+
),
231+
),
232+
)
233+
)
234+
def test_chaindb_persist_unexecuted_block(chain,
235+
chain_without_block_validation_factory,
236+
funded_address,
237+
funded_address_private_key,
238+
use_persist_unexecuted_block):
239+
240+
# We need one chain to create blocks and a second one with a pristine database to test
241+
# persisting blocks that have not been executed.
242+
second_chain = chain_without_block_validation_factory(AtomicDB())
243+
assert chain.get_canonical_head() == second_chain.get_canonical_head()
244+
assert chain != second_chain
245+
246+
NUMBER_BLOCKS_IN_CHAIN = 5
247+
TRANSACTIONS_IN_BLOCK = 10
248+
REQUIRED_BLOCK_NUMBER = 2
249+
REQUIRED_RECEIPT_INDEX = 3
250+
251+
for (block, receipts) in mine_blocks_with_receipts(
252+
chain,
253+
NUMBER_BLOCKS_IN_CHAIN,
254+
TRANSACTIONS_IN_BLOCK,
255+
funded_address,
256+
funded_address_private_key,
257+
):
258+
if block.header.block_number == REQUIRED_BLOCK_NUMBER:
259+
actual_receipt = receipts[REQUIRED_RECEIPT_INDEX]
260+
actual_tx = block.transactions[REQUIRED_RECEIPT_INDEX]
261+
tx_class = block.transaction_class
262+
263+
if use_persist_unexecuted_block:
264+
second_chain.chaindb.persist_unexecuted_block(block, receipts)
265+
else:
266+
# We just use this for an XFAIL to prove `persist_block` does not properly
267+
# persist blocks that were not executed.
268+
second_chain.chaindb.persist_block(block)
269+
270+
chaindb_retrieved_tx = second_chain.chaindb.get_transaction_by_index(
271+
REQUIRED_BLOCK_NUMBER, REQUIRED_RECEIPT_INDEX, tx_class)
272+
assert chaindb_retrieved_tx == actual_tx
273+
274+
# Check that the receipt retrieved is indeed the actual one
275+
chaindb_retrieved_receipt = second_chain.chaindb.get_receipt_by_index(
276+
REQUIRED_BLOCK_NUMBER,
277+
REQUIRED_RECEIPT_INDEX,
278+
)
279+
assert chaindb_retrieved_receipt == actual_receipt
280+
281+
# Raise error if block number is not found
282+
with pytest.raises(ReceiptNotFound):
283+
second_chain.chaindb.get_receipt_by_index(
284+
NUMBER_BLOCKS_IN_CHAIN + 1,
285+
REQUIRED_RECEIPT_INDEX,
286+
)
287+
288+
# Raise error if receipt index is out of range
289+
with pytest.raises(ReceiptNotFound):
290+
second_chain.chaindb.get_receipt_by_index(
291+
NUMBER_BLOCKS_IN_CHAIN,
292+
TRANSACTIONS_IN_BLOCK + 1,
293+
)

0 commit comments

Comments
 (0)