From 1fc4f924b21c0cd7066830fa120a97a7e38d68a4 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Thu, 24 Nov 2022 16:24:02 +0100 Subject: [PATCH] bump `nim-eth`, extend empty block fallback for Capella Implements `emptyPayloadToBlockHeader` for Capella. https://github.com/status-im/nim-eth/pull/562 --- AllTests-mainnet.md | 7 +- beacon_chain/spec/helpers.nim | 116 +++++++++++++++---- beacon_chain/validators/validator_duties.nim | 12 +- tests/test_helpers.nim | 66 +++++++++-- tests/testblockutil.nim | 4 +- vendor/nim-eth | 2 +- 6 files changed, 165 insertions(+), 42 deletions(-) diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 79fc206709..556f70de15 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -447,11 +447,12 @@ OK: 12/12 Fail: 0/12 Skip: 0/12 OK: 1/1 Fail: 0/1 Skip: 0/1 ## Spec helpers ```diff -+ build_empty_execution_payload OK ++ build_empty_execution_payload - Bellatrix OK ++ build_empty_execution_payload - Capella OK + build_proof - BeaconState OK + integer_squareroot OK ``` -OK: 3/3 Fail: 0/3 Skip: 0/3 +OK: 4/4 Fail: 0/4 Skip: 0/4 ## Specific field types ```diff + root update OK @@ -613,4 +614,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 338/343 Fail: 0/343 Skip: 5/343 +OK: 339/344 Fail: 0/344 Skip: 5/344 diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 596c11f735..a9d032cd0c 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -18,7 +18,7 @@ import # Status libraries stew/[bitops2, byteutils, endians2, objects, saturation_arith], chronicles, - eth/eip1559, eth/common/[eth_types, eth_types_rlp], + eth/eip1559, eth/common/[eth_types, eth_types_rlp], eth/trie/[db, hexary], # Internal ./datatypes/[phase0, altair, bellatrix, capella], "."/[eth2_merkleization, forks, ssz_codec] @@ -29,6 +29,7 @@ export forks, eth2_merkleization, ssz_codec type + ExecutionWithdrawal = eth_types.Withdrawal ExecutionBlockHeader = eth_types.BlockHeader FinalityCheckpoints* = object @@ -360,32 +361,101 @@ func compute_timestamp_at_slot*(state: ForkyBeaconState, slot: Slot): uint64 = let slots_since_genesis = slot - GENESIS_SLOT state.genesis_time + slots_since_genesis * SECONDS_PER_SLOT +func gweiToWei*(gwei: Gwei): UInt256 = + gwei.u256 * 1_000_000_000.u256 + +func toExecutionWithdrawal*( + withdrawal: capella.Withdrawal): ExecutionWithdrawal = + ExecutionWithdrawal( + index: withdrawal.index, + validatorIndex: withdrawal.validatorIndex, + address: EthAddress withdrawal.address.data, + amount: gweiToWei withdrawal.amount) + +# https://eips.ethereum.org/EIPS/eip-4895 +proc computeWithdrawalsTrieRoot*( + payload: capella.ExecutionPayload): Hash256 = + if payload.withdrawals.len == 0: + return EMPTY_ROOT_HASH + + var tr = initHexaryTrie(newMemoryDB()) + for i, withdrawal in payload.withdrawals: + try: + tr.put(rlp.encode(i), rlp.encode(toExecutionWithdrawal(withdrawal))) + except RlpError as exc: + doAssert false, "HexaryTrie.put failed: " & $exc.msg + tr.rootHash() + proc emptyPayloadToBlockHeader*( - payload: bellatrix.ExecutionPayload): ExecutionBlockHeader = + payload: bellatrix.ExecutionPayload | capella.ExecutionPayload +): ExecutionBlockHeader = + static: # `GasInt` is signed. We only use it for hashing. + doAssert sizeof(GasInt) == sizeof(payload.gas_limit) + doAssert sizeof(GasInt) == sizeof(payload.gas_used) + ## This function assumes that the payload is empty! doAssert payload.transactions.len == 0 + let + txRoot = EMPTY_ROOT_HASH + withdrawalsRoot = + when payload is bellatrix.ExecutionPayload: + none(Hash256) + else: + some payload.computeWithdrawalsTrieRoot() + ExecutionBlockHeader( - parentHash : payload.parent_hash, - ommersHash : EMPTY_UNCLE_HASH, - coinbase : EthAddress payload.fee_recipient.data, - stateRoot : payload.state_root, - txRoot : EMPTY_ROOT_HASH, - receiptRoot : payload.receipts_root, - bloom : payload.logs_bloom.data, - difficulty : default(DifficultyInt), - blockNumber : payload.block_number.u256, - gasLimit : GasInt payload.gas_limit, - gasUsed : GasInt payload.gas_used, - timestamp : fromUnix(int64.saturate payload.timestamp), - extraData : payload.extra_data.asSeq, - mixDigest : payload.prev_randao, # EIP-4399 redefine `mixDigest` -> `prevRandao` - nonce : default(BlockNonce), - fee : some payload.base_fee_per_gas - ) + parentHash : payload.parent_hash, + ommersHash : EMPTY_UNCLE_HASH, + coinbase : EthAddress payload.fee_recipient.data, + stateRoot : payload.state_root, + txRoot : txRoot, + receiptRoot : payload.receipts_root, + bloom : payload.logs_bloom.data, + difficulty : default(DifficultyInt), + blockNumber : payload.block_number.u256, + gasLimit : cast[GasInt](payload.gas_limit), + gasUsed : cast[GasInt](payload.gas_used), + timestamp : fromUnix(int64.saturate payload.timestamp), + extraData : payload.extra_data.asSeq, + mixDigest : payload.prev_randao, # EIP-4399 `mixDigest` -> `prevRandao` + nonce : default(BlockNonce), + fee : some payload.base_fee_per_gas, + withdrawalsRoot: withdrawalsRoot) + +func build_empty_execution_payload*( + state: bellatrix.BeaconState, + feeRecipient: Eth1Address): bellatrix.ExecutionPayload = + ## Assuming a pre-state of the same slot, build a valid ExecutionPayload + ## without any transactions. + let + latest = state.latest_execution_payload_header + timestamp = compute_timestamp_at_slot(state, state.slot) + randao_mix = get_randao_mix(state, get_current_epoch(state)) + base_fee = calcEip1599BaseFee(GasInt.saturate latest.gas_limit, + GasInt.saturate latest.gas_used, + latest.base_fee_per_gas) + + var payload = bellatrix.ExecutionPayload( + parent_hash: latest.block_hash, + fee_recipient: bellatrix.ExecutionAddress(data: distinctBase(feeRecipient)), + state_root: latest.state_root, # no changes to the state + receipts_root: EMPTY_ROOT_HASH, + block_number: latest.block_number + 1, + prev_randao: randao_mix, + gas_limit: latest.gas_limit, # retain same limit + gas_used: 0, # empty block, 0 gas + timestamp: timestamp, + base_fee_per_gas: base_fee) + + payload.block_hash = rlpHash emptyPayloadToBlockHeader(payload) + + payload -func build_empty_execution_payload*[BS, EP]( - state: BS, feeRecipient: Eth1Address): EP = +proc build_empty_execution_payload*( + state: capella.BeaconState, + feeRecipient: Eth1Address, + expectedWithdrawals: seq[capella.Withdrawal]): capella.ExecutionPayload = ## Assuming a pre-state of the same slot, build a valid ExecutionPayload ## without any transactions. let @@ -396,7 +466,7 @@ func build_empty_execution_payload*[BS, EP]( GasInt.saturate latest.gas_used, latest.base_fee_per_gas) - var payload = EP( + var payload = capella.ExecutionPayload( parent_hash: latest.block_hash, fee_recipient: bellatrix.ExecutionAddress(data: distinctBase(feeRecipient)), state_root: latest.state_root, # no changes to the state @@ -407,6 +477,8 @@ func build_empty_execution_payload*[BS, EP]( gas_used: 0, # empty block, 0 gas timestamp: timestamp, base_fee_per_gas: base_fee) + for withdrawal in expectedWithdrawals: + doAssert payload.withdrawals.add withdrawal payload.block_hash = rlpHash emptyPayloadToBlockHeader(payload) diff --git a/beacon_chain/validators/validator_duties.nim b/beacon_chain/validators/validator_duties.nim index 2ef395baa2..6324146121 100644 --- a/beacon_chain/validators/validator_duties.nim +++ b/beacon_chain/validators/validator_duties.nim @@ -388,11 +388,15 @@ proc getExecutionPayload[T]( template empty_execution_payload(): auto = withState(proposalState[]): - when stateFork >= BeaconStateFork.Bellatrix: - build_empty_execution_payload[typeof forkyState.data, T]( - forkyState.data, feeRecipient) + when stateFork >= BeaconStateFork.Capella: + raiseAssert $capellaImplementationMissing # expected withdrawals + let expectedWithdrawals = newSeq[Withdrawal]() # TODO Pass into proc! + build_empty_execution_payload( + forkyState.data, feeRecipient, expectedWithdrawals) + elif stateFork >= BeaconStateFork.Bellatrix: + build_empty_execution_payload(forkyState.data, feeRecipient) else: - default(T) + raiseAssert "Fork " & $stateFork & " lacks `ExecutionPayload`" if node.eth1Monitor.isNil: beacon_block_payload_errors.inc() diff --git a/tests/test_helpers.nim b/tests/test_helpers.nim index 4bdd814688..f99f360ab2 100644 --- a/tests/test_helpers.nim +++ b/tests/test_helpers.nim @@ -10,10 +10,11 @@ import # Status libraries stew/bitops2, + eth/common/eth_types as commonEthTypes, eth/common/eth_types_rlp, web3/ethtypes, # Beacon chain internals ../beacon_chain/spec/[forks, helpers, state_transition], - ../beacon_chain/spec/datatypes/bellatrix, + ../beacon_chain/spec/datatypes/[bellatrix, capella], # Test utilities ./unittest2, mocking/mock_genesis @@ -61,20 +62,67 @@ suite "Spec helpers": i += 1 process(state, state.numLeaves) - test "build_empty_execution_payload": + test "build_empty_execution_payload - Bellatrix": var cfg = defaultRuntimeConfig cfg.ALTAIR_FORK_EPOCH = GENESIS_EPOCH cfg.BELLATRIX_FORK_EPOCH = GENESIS_EPOCH let state = newClone(initGenesisState(cfg = cfg).bellatrixData) - template testCase(recipient: Eth1Address): untyped = - block: - let payload = build_empty_execution_payload[ - typeof state[].data, bellatrix.ExecutionPayload]( - state[].data, recipient) - check payload.fee_recipient == - bellatrix.ExecutionAddress(data: distinctBase(recipient)) + proc testCase(recipient: Eth1Address) = + let payload = build_empty_execution_payload(state[].data, recipient) + check payload.fee_recipient == + bellatrix.ExecutionAddress(data: distinctBase(recipient)) testCase default(Eth1Address) testCase Eth1Address.fromHex("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b") + + test "build_empty_execution_payload - Capella": + var cfg = defaultRuntimeConfig + cfg.ALTAIR_FORK_EPOCH = GENESIS_EPOCH + cfg.BELLATRIX_FORK_EPOCH = GENESIS_EPOCH + cfg.CAPELLA_FORK_EPOCH = GENESIS_EPOCH + + let + state = newClone(initGenesisState(cfg = cfg).capellaData) + recipient = Eth1Address.fromHex( + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b") + + proc testCase(withdrawals: seq[capella.Withdrawal]) = + let payload = build_empty_execution_payload( + state[].data, recipient, withdrawals) + check payload.fee_recipient == + bellatrix.ExecutionAddress(data: distinctBase(recipient)) + for i, withdrawal in withdrawals: + check payload.withdrawals[i] == withdrawal + + let elHeader = emptyPayloadToBlockHeader(payload) + check elHeader.withdrawalsRoot.isSome + if withdrawals.len == 0: + check elHeader.withdrawalsRoot.get == EMPTY_ROOT_HASH + else: + check elHeader.withdrawalsRoot.get != EMPTY_ROOT_HASH + check elHeader.blockHash == payload.block_hash + + var bellatrixHeader = elHeader + bellatrixHeader.withdrawalsRoot.reset() + check elHeader.blockHash != rlpHash bellatrixHeader + + testCase @[] + testCase @[ + capella.Withdrawal( + index: 42, + validatorIndex: 1337, + address: bellatrix.ExecutionAddress(data: distinctBase(recipient)), + amount: 25.Gwei)] + testCase @[ + capella.Withdrawal( + index: 1, + validatorIndex: 1, + address: bellatrix.ExecutionAddress(data: distinctBase(recipient)), + amount: 1.Gwei), + capella.Withdrawal( + index: 2, + validatorIndex: 2, + address: bellatrix.ExecutionAddress(data: distinctBase(recipient)), + amount: 2.Gwei)] diff --git a/tests/testblockutil.nim b/tests/testblockutil.nim index 5782ae85bc..1fb87e84cc 100644 --- a/tests/testblockutil.nim +++ b/tests/testblockutil.nim @@ -146,9 +146,7 @@ proc addTestBlock*( cfg.BELLATRIX_FORK_EPOCH * SLOTS_PER_EPOCH + 10: if is_merge_transition_complete(forkyState.data): const feeRecipient = default(Eth1Address) - build_empty_execution_payload[ - bellatrix.BeaconState, bellatrix.ExecutionPayload]( - forkyState.data, feeRecipient) + build_empty_execution_payload(forkyState.data, feeRecipient) else: build_empty_merge_execution_payload(forkyState.data) else: diff --git a/vendor/nim-eth b/vendor/nim-eth index 70b83a4efb..6499ee2bc5 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 70b83a4efbb2912bd33d3a2acfb7aad9b40f4bc4 +Subproject commit 6499ee2bc5d264fdc68f5f08b647222a5c5252fa