Skip to content

Commit e60423b

Browse files
committed
Mine all transactions in one batch
Provides a meaningful speedup during tests, and especially in the benchmarks. Requires a rework of the VM.mine_block() API to stop relying on the side-effect of setting vm._block.
1 parent 16e69e0 commit e60423b

File tree

8 files changed

+150
-22
lines changed

8 files changed

+150
-22
lines changed

eth/abc.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,16 @@ class BlockAndMetaWitness(NamedTuple):
480480
meta_witness: MetaWitnessAPI
481481

482482

483+
class BlockPersistResult(NamedTuple):
484+
"""
485+
After persisting a block into the active chain, this information
486+
becomes available.
487+
"""
488+
imported_block: BlockAPI
489+
new_canonical_blocks: Tuple[BlockAPI, ...]
490+
old_canonical_blocks: Tuple[BlockAPI, ...]
491+
492+
483493
class BlockImportResult(NamedTuple):
484494
"""
485495
After importing and persisting a block into the active chain, this information
@@ -2785,9 +2795,9 @@ def import_block(self, block: BlockAPI) -> BlockAndMetaWitness:
27852795
...
27862796

27872797
@abstractmethod
2788-
def mine_block(self, *args: Any, **kwargs: Any) -> BlockAndMetaWitness:
2798+
def mine_block(self, block: BlockAPI, *args: Any, **kwargs: Any) -> BlockAndMetaWitness:
27892799
"""
2790-
Mine the current block. Proxies to self.pack_block method.
2800+
Mine the given block. Proxies to self.pack_block method.
27912801
"""
27922802
...
27932803

@@ -3586,6 +3596,24 @@ def __init__(self, base_db: AtomicDatabaseAPI, header: BlockHeaderAPI = None) ->
35863596
"""
35873597
...
35883598

3599+
@abstractmethod
3600+
def mine_all(
3601+
self,
3602+
transactions: Sequence[SignedTransactionAPI],
3603+
*args: Any,
3604+
parent_header: BlockHeaderAPI = None,
3605+
**kwargs: Any,
3606+
) -> Tuple[BlockImportResult, Tuple[ReceiptAPI, ...], Tuple[ComputationAPI, ...]]:
3607+
"""
3608+
Build a block with the given transactions, and mine it.
3609+
3610+
Optionally, supply the parent block header to mine on top of.
3611+
3612+
This is much faster than individually running :meth:`apply_transaction`
3613+
and then :meth:`mine_block`.
3614+
"""
3615+
...
3616+
35893617
@abstractmethod
35903618
def apply_transaction(self,
35913619
transaction: SignedTransactionAPI

eth/chains/base.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
AtomicDatabaseAPI,
4545
BlockHeaderAPI,
4646
BlockImportResult,
47+
BlockPersistResult,
4748
ChainAPI,
4849
ChainDatabaseAPI,
4950
ConsensusContextAPI,
@@ -481,17 +482,27 @@ def import_block(self,
481482
except ValidationError:
482483
self.logger.warning("Proposed %s doesn't follow EVM rules, rejecting...", block)
483484
raise
484-
self.validate_block(imported_block)
485+
486+
persist_result = self.persist_block(imported_block, perform_validation)
487+
return BlockImportResult(*persist_result, block_result.meta_witness)
488+
489+
def persist_block(
490+
self,
491+
block: BlockAPI,
492+
perform_validation: bool = True) -> BlockPersistResult:
493+
494+
if perform_validation:
495+
self.validate_block(block)
485496

486497
(
487498
new_canonical_hashes,
488499
old_canonical_hashes,
489-
) = self.chaindb.persist_block(imported_block)
500+
) = self.chaindb.persist_block(block)
490501

491502
self.logger.debug(
492-
'IMPORTED_BLOCK: number %s | hash %s',
493-
imported_block.number,
494-
encode_hex(imported_block.hash),
503+
'Persisted block: number %s | hash %s',
504+
block.number,
505+
encode_hex(block.hash),
495506
)
496507

497508
new_canonical_blocks = tuple(
@@ -505,11 +516,10 @@ def import_block(self,
505516
in old_canonical_hashes
506517
)
507518

508-
return BlockImportResult(
509-
imported_block=imported_block,
519+
return BlockPersistResult(
520+
imported_block=block,
510521
new_canonical_blocks=new_canonical_blocks,
511522
old_canonical_blocks=old_canonical_blocks,
512-
meta_witness=block_result.meta_witness,
513523
)
514524

515525
#
@@ -667,11 +677,43 @@ def import_block(self,
667677
self.header = self.ensure_header()
668678
return result
669679

680+
def mine_all(
681+
self,
682+
transactions: Sequence[SignedTransactionAPI],
683+
*args: Any,
684+
parent_header: BlockHeaderAPI = None,
685+
**kwargs: Any,
686+
) -> Tuple[BlockImportResult, Tuple[ReceiptAPI, ...], Tuple[ComputationAPI, ...]]:
687+
688+
if parent_header is None:
689+
base_header = self.header
690+
else:
691+
base_header = self.create_header_from_parent(parent_header)
692+
693+
vm = self.get_vm(base_header)
694+
695+
new_header, receipts, computations = vm.apply_all_transactions(transactions, base_header)
696+
filled_block = vm.set_block_transactions(vm.get_block(), new_header, transactions, receipts)
697+
698+
block_result = vm.mine_block(filled_block, *args, **kwargs)
699+
imported_block = block_result.block
700+
701+
block_persist_result = self.persist_block(imported_block)
702+
block_import_result = BlockImportResult(*block_persist_result, block_result.meta_witness)
703+
704+
self.header = self.create_header_from_parent(imported_block.header)
705+
return (block_import_result, receipts, computations)
706+
670707
def mine_block(self, *args: Any, **kwargs: Any) -> BlockAPI:
708+
"""
709+
Mine whatever transactions have been incrementally applied so far.
710+
"""
671711
return self.mine_block_extended(*args, **kwargs).block
672712

673713
def mine_block_extended(self, *args: Any, **kwargs: Any) -> BlockAndMetaWitness:
674-
mine_result = self.get_vm(self.header).mine_block(*args, **kwargs)
714+
vm = self.get_vm(self.header)
715+
current_block = vm.get_block()
716+
mine_result = vm.mine_block(current_block, *args, **kwargs)
675717
mined_block = mine_result.block
676718

677719
self.validate_block(mined_block)

eth/tools/builder/chain/builders.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,10 +366,8 @@ def mine_block(chain: MiningChainAPI, **kwargs: Any) -> MiningChainAPI:
366366
raise ValidationError('`mine_block` may only be used on MiningChain instances')
367367

368368
transactions = kwargs.pop('transactions', ())
369-
for tx in transactions:
370-
chain.apply_transaction(tx)
371369

372-
chain.mine_block(**kwargs)
370+
chain.mine_all(transactions, **kwargs)
373371
return chain
374372

375373

eth/tools/factories/transaction.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ def new_transaction(
1515
gas_price=10,
1616
gas=100000,
1717
data=b'',
18+
nonce=None,
1819
chain_id=None):
1920
"""
2021
Create and return a transaction sending amount from <from_> to <to>.
2122
2223
The transaction will be signed with the given private key.
2324
"""
24-
nonce = vm.state.get_nonce(from_)
25+
if nonce is None:
26+
nonce = vm.state.get_nonce(from_)
27+
2528
tx = vm.create_unsigned_transaction(
2629
nonce=nonce,
2730
gas_price=gas_price,

eth/vm/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,17 +305,17 @@ def import_block(self, block: BlockAPI) -> BlockAndMetaWitness:
305305
# run all of the transactions.
306306
new_header, receipts, _ = self.apply_all_transactions(block.transactions, header)
307307

308-
self._block = self.set_block_transactions(
308+
block_with_transactions = self.set_block_transactions(
309309
self.get_block(),
310310
new_header,
311311
block.transactions,
312312
receipts,
313313
)
314314

315-
return self.mine_block()
315+
return self.mine_block(block_with_transactions)
316316

317-
def mine_block(self, *args: Any, **kwargs: Any) -> BlockAndMetaWitness:
318-
packed_block = self.pack_block(self.get_block(), *args, **kwargs)
317+
def mine_block(self, block: BlockAPI, *args: Any, **kwargs: Any) -> BlockAndMetaWitness:
318+
packed_block = self.pack_block(block, *args, **kwargs)
319319

320320
block_result = self.finalize_block(packed_block)
321321

tests/core/chain-object/test_chain.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from eth_utils import decode_hex
55

66
from eth import constants
7+
from eth.abc import MiningChainAPI
78
from eth.chains.mainnet import MAINNET_GENESIS_HEADER
89
from eth.chains.ropsten import ROPSTEN_GENESIS_HEADER
910
from eth.exceptions import (
@@ -38,6 +39,15 @@ def tx(chain, funded_address, funded_address_private_key):
3839
return new_transaction(vm, from_, recipient, amount, funded_address_private_key)
3940

4041

42+
@pytest.fixture()
43+
def tx2(chain, funded_address, funded_address_private_key):
44+
recipient = b'\x88' * 20
45+
amount = 100
46+
vm = chain.get_vm()
47+
from_ = funded_address
48+
return new_transaction(vm, from_, recipient, amount, funded_address_private_key, nonce=1)
49+
50+
4151
@pytest.mark.xfail(reason="modification to initial allocation made the block fixture invalid")
4252
def test_import_block_validation(valid_chain, funded_address, funded_address_initial_balance):
4353
block = rlp.decode(valid_block_rlp, sedes=FrontierBlock)
@@ -63,6 +73,7 @@ def test_import_block(chain, tx):
6373
else:
6474
# working on a non-mining chain, so we have to build the block to apply manually
6575
new_block, receipts, computations = chain.build_block_with_transactions([tx])
76+
assert len(computations) == 1
6677
computations[0].raise_if_error()
6778

6879
block_import_result = chain.import_block(new_block)
@@ -74,6 +85,48 @@ def test_import_block(chain, tx):
7485
assert chain.get_canonical_transaction(tx.hash) == tx
7586

7687

88+
def test_mine_all(chain, tx, tx2, funded_address):
89+
if hasattr(chain, 'mine_all'):
90+
start_balance = chain.get_vm().state.get_balance(funded_address)
91+
92+
mine_result = chain.mine_all([tx, tx2])
93+
block = mine_result[0].imported_block
94+
95+
assert block.transactions == (tx, tx2)
96+
assert chain.get_block_by_hash(block.hash) == block
97+
assert chain.get_canonical_block_by_number(block.number) == block
98+
assert chain.get_canonical_transaction(tx.hash) == tx
99+
100+
end_balance = chain.get_vm().state.get_balance(funded_address)
101+
expected_spend = 2 * (100 + 21000 * 10) # sent + gas * gasPrice
102+
103+
assert start_balance - end_balance == expected_spend
104+
elif isinstance(chain, MiningChainAPI):
105+
raise AssertionError() # Mining chains should have the 'mine_all' method
106+
107+
108+
def test_mine_all_uncle(chain, tx, tx2, funded_address):
109+
if hasattr(chain, 'mine_all'):
110+
starting_tip = chain.get_canonical_head()
111+
canonical = chain.mine_all([tx])
112+
uncled = chain.mine_all([], parent_header=starting_tip)
113+
uncled_header = uncled[0].imported_block.header
114+
115+
new_tip = chain.mine_all(
116+
[tx2],
117+
parent_header=canonical[0].imported_block.header,
118+
uncles=[uncled_header],
119+
)
120+
121+
block = new_tip[0].imported_block
122+
123+
assert block.transactions == (tx2,)
124+
assert chain.get_block_by_hash(block.hash) == block
125+
assert block.uncles == (uncled_header,)
126+
elif isinstance(chain, MiningChainAPI):
127+
raise AssertionError() # Mining chains should have the 'mine_all' method
128+
129+
77130
def test_empty_transaction_lookups(chain):
78131
with pytest.raises(TransactionNotFound):
79132
chain.get_canonical_transaction(b'\0' * 32)

tests/core/vm/test_rewards.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,15 @@ def test_uncle_block_inclusion_validity(vm_fn):
229229
uncle = fork_chain.get_block_header_by_hash(fork_chain.header.parent_hash)
230230
assert uncle.block_number == 2
231231

232+
chain = build(
233+
chain,
234+
# Mines blocks from 3 to 8 (both numbers inclusive)
235+
mine_blocks(6),
236+
)
237+
232238
with pytest.raises(ValidationError):
233239
chain = build(
234240
chain,
235-
# Mines blocks from 3 to 8 (both numbers inclusive)
236-
mine_blocks(6),
237241
# Mine block 9 with uncle
238242
mine_block(uncles=[uncle]),
239243
)

tests/core/vm/test_vm_state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def state(chain_without_block_validation):
2121
def test_block_properties(chain_without_block_validation):
2222
chain = chain_without_block_validation
2323
vm = chain.get_vm()
24-
mined_block = vm.mine_block().block
24+
mined_block = vm.mine_block(vm.get_block()).block
2525
block_import_result = chain.import_block(mined_block)
2626
block = block_import_result.imported_block
2727

0 commit comments

Comments
 (0)