-
Notifications
You must be signed in to change notification settings - Fork 984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add hexary trie roots for lists in ExecutionPayloadHeader
#3078
Conversation
The `ExecutionPayloadHeader` currently contains a SSZ merkle root for `transactions_root` / `withdrawals_root`. While this is fine for the purpose of tracking the `latest_execution_payload_header` as part of `BeaconState`, it introduces challenges when trying to extend the Engine API to support light client based EL client implementations. By tracking the RLP hash for transactions and withdrawals as well, it would become possible to introduce an `engine_newPayloadHeader` API that allows passing the full EL block header, unlocking LES use cases.
Note that this change leads to Maybe cleaner to just do the switch beyond 16 now instead of with EIP4844 / something else that would promote actually using those generalized indices (so far, they are not used I think). |
specs/capella/beacon-chain.md
Outdated
transactions_hash: Bytes32 # [New in Capella] | ||
withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Capella] | ||
withdrawals_hash: Bytes32 # [New in Capella] | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'd also need to extend the engine API to include validation of these values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, engine API is being extended as part of Capella anyway, so that shouldn't be a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matching engine API PR: ethereum/execution-apis#318
how much worse is it to just extend the light client types so that they have the execution payload header and a Merkle proof showing its correctness? in general we don't want to be modifying or extending core types if the cost is only a few bytes |
transactions_hash is an RLP hash, and does not use SSZ merkle proof, so that alternative would consist of:
Simply passing the existing Hence the idea to have the EL expose the transactions RLP hash via engine API, so it is part of the |
maybe I've lost the use case and so missing some critical constraint but can we not just include a Merkle proof from sync committee's you can also continue the proof chain down into the header to prove any individual transaction if desired the CL light client could provide these verified headers to the EL for the use case where we just want to feed execution data to the EL |
The use case is driving LES via engine API. LES expects full EL block headers, not just state root; this way, no further network queries are necessary on the LES side — see ethereum/execution-apis#318 (comment) — Mode of operation could be a CL light client that tracks the latest But, currently, there is no easy access to the EL block header, because the |
yeah I see -- it would be nice to just be able to provide the hash and a proof that it is in the correct place inside the preimage of the block hash, simply because each and every thing we add to the state/block has a huge cost around implementation, security, testing and long-term maintenance I wonder what it would look like to make a SNARK or even STARK of this root -> hash equivalence... |
Who would be the entity providing the RLP hash in that situation though? If it's the CL's providing the LC data, it would mean converting the The alternative proposal here exposes the existing RLP Note also the matching behaviour for As for SNARK/STARK, it already has to do that equivalence today, to validate:
The added value is essentially that the EL block header is constructable from the CL |
Updated the corresponding engine API call with the corresponding change, if this PR would be adopted: The validation of the RLP
In EIP 4895 it is visible that the corresponding RLP
|
The alternative is for light client update provider to construct them but that this is more protocol |
Yep, having provider compute them would also be possible, but would require adding RLP hashing capabilities to the "light client update provider" (including CL full nodes), as well as into all light clients (even non-LES ones, otherwise they can't participate in libp2p gossip as they can't validate the gossip). |
After a conversation with @etan-status in Discord, we identified two goals that this proposal aims at:
As for the (1), I agree with @ralexstokes that LC protocol may provide a proof that beacon block (state) root commits to a certain For the (2), does LES protocol require |
re (2), @zsfelfoldi |
BTW, also needs Patricia Trie support to produce the additional hashes (in the BN). def compute_trie_root_from_indexed_data(data):
trie = Trie.from([(i, obj) for i, obj in enumerate(data)])
return trie.root
execution_payload_header.withdrawals_root = \
compute_trie_root_from_indexed_data(execution_payload.withdrawals) (and, similar for |
Alternate design: trie roots NOT part of
CL light client would receive this object. To validate:
The EL needs to compute In the alternate design here, the This replicates some of the EL work as part of the CL and LC logic, but at least keeps For the CL, added requirements:
For the LC, added requirements:
Production (CL) def compute_trie_root_from_indexed_data(data):
"""
Computes the root hash of `patriciaTrie(rlp(Index) => Data)` for a data array.
"""
t = HexaryTrie(db={})
for i, obj in enumerate(data):
k = encode(i, big_endian_int)
t.set(k, obj)
return t.root_hash
def get_withdrawal_rlp(withdrawal):
withdrawal_rlp = [
# index
(big_endian_int, withdrawal.index),
# validator_index
(big_endian_int, withdrawal.validator_index),
# address
(Binary(20, 20), withdrawal.address),
# amount
(big_endian_int, uint256(withdrawal.amount) * (10**9)),
]
sedes = List([schema for schema, _ in withdrawal_rlp])
values = [value for _, value in withdrawal_rlp]
return encode(values, sedes)
def create_light_client_header(block):
beacon = BeaconBlockHeader(
slot=block.message.slot,
proposer_index=block.message.proposer_index,
parent_root=block.message.parent_root,
state_root=block.message.state_root,
body_root=hash_tree_root(block.message.body),
)
payload = block.message.body.execution_payload
execution = ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
fee_recipient=payload.fee_recipient,
state_root=payload.state_root,
receipts_root=payload.receipts_root,
logs_bloom=payload.logs_bloom,
prev_randao=payload.prev_randao,
block_number=payload.block_number,
gas_limit=payload.gas_limit,
gas_used=payload.gas_used,
timestamp=payload.timestamp,
extra_data=payload.extra_data,
base_fee_per_gas=payload.base_fee_per_gas,
block_hash=payload.block_hash,
transactions_root=hash_tree_root(payload.transactions),
withdrawals_root=hash_tree_root(payload.withdrawals),
)
execution_branch = compute_merkle_proof_for_block_body(
block.message.body,
EXECUTION_PAYLOAD_INDEX,
)
transactions_trie_root = compute_trie_root_from_indexed_data(payload.transactions)
withdrawals_encoded = [get_withdrawal_rlp(spec, withdrawal) for withdrawal in payload.withdrawals]
withdrawals_trie_root = compute_trie_root_from_indexed_data(withdrawals_encoded)
return LightClientHeader(
beacon=beacon,
execution=execution,
execution_branch=execution_branch,
transactions_trie_root=transactions_trie_root,
withdrawals_trie_root=withdrawals_trie_root,
) The light client would then validate that the obtained def is_light_client_header_valid(header): # signature check out of scope
# Validate that `header.execution` corresponds to `header.beacon`
if not is_valid_merkle_branch(
leaf=hash_tree_root(header.execution),
branch=header.execution_branch,
depth=floorlog2(EXECUTION_PAYLOAD_INDEX),
index=get_subtree_index(EXECUTION_PAYLOAD_INDEX),
root=header.beacon.body_root,
):
return False
# Validate that `header.transactions_trie_root` and `header.withdrawals_trie_root`
# correspond to `header.execution`
payload_header = light_client_header.execution
execution_payload_header_rlp = [
# parent_hash
(Binary(32, 32), payload_header.parent_hash),
# ommers_hash
(Binary(32, 32), bytes.fromhex("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")),
# coinbase
(Binary(20, 20), payload_header.fee_recipient),
# state_root
(Binary(32, 32), payload_header.state_root),
# txs_root
(Binary(32, 32), header.transactions_trie_root),
# receipts_root
(Binary(32, 32), payload_header.receipts_root),
# logs_bloom
(Binary(256, 256), payload_header.logs_bloom),
# difficulty
(big_endian_int, 0),
# number
(big_endian_int, payload_header.block_number),
# gas_limit
(big_endian_int, payload_header.gas_limit),
# gas_used
(big_endian_int, payload_header.gas_used),
# timestamp
(big_endian_int, payload_header.timestamp),
# extradata
(Binary(0, 32), payload_header.extra_data),
# prev_randao
(Binary(32, 32), payload_header.prev_randao),
# nonce
(Binary(8, 8), bytes.fromhex("0000000000000000")),
# base_fee_per_gas
(big_endian_int, payload_header.base_fee_per_gas),
# withdrawals_root
(Binary(32, 32), header.withdrawals_trie_root)
]
sedes = List([schema for schema, _ in execution_payload_header_rlp])
values = [value for _, value in execution_payload_header_rlp]
encoded = encode(values, sedes)
computed_block_hash = spec.Hash32(keccak(encoded))
return computed_block_hash == payload_header.block_hash See also: #3126 (file: |
Alternate design above could interfere with split block storage, in which case the CL is no longer able to reconstruct the hexary tries needed for Overall, it would arguably be better to avoid conflicting serialization strategies across CL and EL. Especially, for the withdrawals which are a completely new addition, I don't see arguments in favor of using RLP / hexary tries / Wei instead of Gwei, and have commented here: https://ethereum-magicians.org/t/eip-4895-beacon-chain-withdrawals-as-system-level-operations/8568/28 As for transactions, given their history, it may make sense to just include the hexary trie root into the Because of aforementioned split block storage aspects, it may also become prohibitive to require CL to serve proofs about tx / withdrawal inclusion, so the SSZ roots may be necessary in EL world anyway. |
ExecutionPayloadHeader
ExecutionPayloadHeader
Superseded by Once EL block header is changed to use SSZ format for |
The
ExecutionPayloadHeader
currently contains a SSZ merkle root fortransactions_root
/withdrawals_root
. While this is fine for the purpose of tracking thelatest_execution_payload_header
as part ofBeaconState
, it introduces challenges when trying to extend the Engine API to support light client based EL client implementations.By tracking the RLP hash for transactions and withdrawals as well, it would become possible to introduce an
engine_newPayloadHeader
API that allows passing the full EL block header, unlocking LES use cases.