stateless-cleints are very much necessary for the decentralisation of ethereum, due to following reasons:
the main motivation of creating a reth-verkle poc is is to develop more working implementation of verkle-integration in EL-clients, which will help in running interop with other clients, further research and allow reth to prepare for the verge, learn more about why statelessness is important here: Why it's so important to go stateless
this project aims to integrate rust-verkle crytographic primitives into reth, and enable it to act as a stateless client.
a basic TL;DR will be:
state_diffs
and verkle_proof
) during block-execution and then serialization of this witness.state_diffs
) obtained from witness, used in block(next) execution against pre_state_root
.state_diffs
are indeed part of trie whose root is our trusted pre_state_root
), proceed with stateless execution:
state_db
from the witness's data.state_db
for block execution rather than using local-chain.end goal of this project will be achieved, if reth is able to join Kaustinen devnet, passive all verkle-execution-spec tests
This section provides required information about structure of various components of rust-verkle that will be subsequently utilised in reth.
These technical specifications will involve following the defined specs and Verkle serialization format in SSZ for making changes in reth:
A block/execution witness (i.e: the verkle proof required to execute a block statelessly) struct will be created, this is an SSZ-encoded serialization of the following ExecutionWitness structure:
ββββclass ExecutionWitness(container):
ββββ state_diff: StateDiff
ββββ verkle_proof: VerkleProof
state_diff
will contain all the pre-state data required to execute the given block, which will then be executed statelessly by other clients(basically verkle trie's, leaf node's key value pair), StateDiff
defination:
ββββMAX_STEMS = 2**16
ββββVERKLE_WIDTH = 256
ββββclass SuffixStateDiff(Container):
ββββ suffix: Byte
ββββ # Null means not currently present
ββββ current_value: Union[Null, Bytes32]
ββββ # Null means value not updated
ββββ new_value: Union[Null, Bytes32]
ββββclass StemStateDiff(Container):
ββββ stem: Stem
ββββ # Valid only if list is sorted by suffixes
ββββ suffix_diffs: List[SuffixStateDiff, VERKLE_WIDTH]
ββββ# Valid only if list is sorted by stems
ββββStateDiff = List[StemStateDiff, MAX_STEMS]
verkle_proof
will contain, all the data needed by the verifier to re-construct a partial view of the pre-state trie(using commitments, root-node, and given block values) for the data present in state_diff
, which will be used to prove that this pre-state data provided is indeed part of the trie whose root-node is the state_root_node
(trusted), already present with the client, VerkleProof
defination:
ββββBandersnatchGroupElement = Bytes32
ββββBandersnatchFieldElement = Bytes32
ββββMAX_COMMITMENTS_PER_STEM = 33 # = 31 for inner nodes + 2 (C1/C2)
ββββIPA_PROOF_DEPTH = 8 # = log2(VERKLE_WIDTH)
ββββclass IpaProof(Container):
ββββ C_L = Vector[BandersnatchGroupElement, IPA_PROOF_DEPTH]
ββββ C_R = Vector[BandersnatchGroupElement, IPA_PROOF_DEPTH]
ββββ final_evaluation = BandersnatchFieldElement
ββββclass VerkleProof(Container):
ββββ // [Group A]
ββββ other_stems: List[Bytes32, MAX_STEMS]
ββββ depth_extension_present: List[uint8, MAX_STEMS]
ββββ commitments_by_path: List[BandersnatchGroupElement, MAX_STEMS * MAX_COMMITMENTS_PER_STEM]
ββββ // [Group B]
ββββ D: BandersnatchGroupElement
ββββ ipa_proof: IpaProof
here, other_stems
, depth_extension_present
, commitments_by_path
are data used to construct this partial-view of verkle-trie, and ipa_proof
is the verkle proof which will be used to open the commitment in the path from provided leaf-nodes to the trie-root, which will prove that the provided data is indeed correct.
for more details regarding above mentioned changes and terms used refer to this great article by Ignacio: Anatomy of a verkle proof
this section discusses pseudo-code for data types and utility/helper functions that would be needed for verkle migration:
trie/verkle.rs
:struct VerkleTrie {
root: Box<dyn VerkleNode>,
db: Box<Database>,
ended: bool,
}
struct ChunkedCode(Vec<u8>);
impl VerkleTrie {
fn to_dot(&self) -> String;
fn new(root: Box<dyn VerkleNode>, db: Box<Database>, ended: bool) -> Self;
fn flatdb_node_resolver(&self, path: &[u8]) -> Result<Vec<u8>, Error>;
fn insert_migrated_leaves(&mut self, leaves: Vec<LeafNode>) -> Result<(), Error>;
fn get_key(&self, key: &[u8]) -> Vec<u8>;
fn get_storage(&self, addr: Address, key: &[u8]) -> Result<Vec<u8>, Error>;
fn get_with_hashed_key(&self, key: &[u8]) -> Result<Vec<u8>, Error>;
fn get_account(&self, addr: Address) -> Result<Option<StateAccount>, Error>;
fn update_account(&mut self, addr: Address, acc: &StateAccount) -> Result<(), Error>;
fn update_stem(&mut self, key: &[u8], values: Vec<Vec<u8>>) -> Result<(), Error>;
fn update_storage(&mut self, address: Address, key: &[u8], value: &[u8]) -> Result<(), Error>;
fn delete_account(&mut self, addr: Address) -> Result<(), Error>;
fn delete_storage(&mut self, addr: Address, key: &[u8]) -> Result<(), Error>;
fn hash(&self) -> Hash;
fn commit(&mut self, _: bool) -> Result<(Hash, Option<NodeSet>), Error>;
fn node_iterator(&self, start_key: &[u8]) -> Result<Box<dyn NodeIterator>, Error>;
fn prove(&self, key: &[u8], proof_db: &mut dyn KeyValueWriter) -> Result<(), Error>;
fn copy(&self) -> Self;
fn is_verkle(&self) -> bool;
fn set_storage_root_conversion(&mut self, addr: Address, root: Hash);
fn clear_storage_root_conversion(&mut self, addr: Address);
fn update_contract_code(&mut self, addr: Address, code_hash: Hash, code: &[u8]) -> Result<(), Error>;
}
fn prove_and_serialize(
pretrie: &VerkleTrie,
posttrie: Option<&VerkleTrie>,
keys: Vec<Vec<u8>>,
resolver: impl Fn(&[u8]) -> Result<Vec<u8>, Error>,
) -> Result<(VerkleProof, StateDiff), Error>;
fn deserialize_and_verify_verkle_proof(
vp: &VerkleProof,
pre_state_root: &[u8],
post_state_root: &[u8],
statediff: StateDiff,
) -> Result<(), Error>;
fn chunkify_code(code: &[u8]) -> ChunkedCode;
need to discuss here what part should go in alloy crates and referenced from there and what should be present in reth crate.
following functions will be added:
function deserialize_and_verify_verkle_proof
: responsible for constructing partial view of pre-state-trie for the given state_diffs
using verkle_proof
and validating correctness of the pre-state data against the pre_state_root
verkle_proof
, pre_state_root
, post_state_root
, state_diffs
as inputs.post_state_root
verify_execution_witness
(see here) from rust-verkle.note:
DeserializeAndVerifyVerkleProof
to go-verkle in this pr, so after proper testing and implementation, I will be doing the same and then using this function as an endpoint from rust-verkle.verify_execution_witness
including pre-state-trie construction.function prove_and_serialize
: responsible for creating serialized verkle_proof
and state_diffs
objects.
pre_trie
, post_trie
and keys
.ipa_multipoint
crate of rust-verkle to get the proof.execute_state_transitions
function.note:
TODO
Tl;Dr: after recieving the next block and the witness data, state execution should shift to stateless architecture and use copy of witness data as stateDB
instead of copy of it's local chain.
client references:
runBlock
function.TODO
later changes related to gas-cost modifications and much more, will be added subsequently
TODO
see geth reference gballet-go-etherum/core/state_processor_test.go
after getting gist of trie-structure please read this article by Ignacio: Anatomy of a Verkle proof this will give all the necessary understanding needed for verkle-migration in EL-clients.
a basic understanding of abstract algebra, elliptic curve-cryptography and number-theory would certainly help
articles related to cryptographic-optimisations:
verkle EIPs:
BLOCKHASH (0x40)
opcode from the EIP-2935 system contract storage and adjust its gas cost to reflect storage access.EIPs related to transition of stateDB during fork:
old EIPs: