Skip to content

Commit b49f97b

Browse files
authored
feat: add support for historical block proofs up to 24h (#36)
* feat: allow to prove onchain up to 24h using beacon roots * chore: add docs
1 parent f348378 commit b49f97b

File tree

23 files changed

+1240
-371
lines changed

23 files changed

+1240
-371
lines changed

Cargo.lock

Lines changed: 553 additions & 103 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ tokio = { version = "1.21", default-features = false, features = [
3131
] }
3232
serde_json = "1.0.94"
3333
serde = { version = "1.0", default-features = false, features = ["derive"] }
34+
reqwest = "0.12.15"
3435
url = "2.3"
3536
hex-literal = "0.4.1"
3637
bincode = "1.3.3"
@@ -96,7 +97,9 @@ revm-primitives = { version = "18.0.0", features = [
9697
# alloy
9798
alloy-primitives = "1.0"
9899
alloy-consensus = { version = "0.14.0", default-features = false }
100+
alloy-contract = { version = "0.14.0", default-features = false }
99101
alloy-eips = { version = "0.14.0", default-features = false }
102+
alloy-node-bindings = { version = "0.14.0", default-features = false }
100103
alloy-provider = { version = "0.14.0", default-features = false, features = [
101104
"reqwest",
102105
] }
@@ -115,6 +118,11 @@ alloy = { version = "0.14.0" }
115118

116119
alloy-evm = { version = "0.4.0", default-features = false }
117120

121+
sha2 = "0.10.8"
122+
beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "ba43147eb71b07e21e156e2904549405f87bc9a6" }
123+
ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "ba43147eb71b07e21e156e2904549405f87bc9a6" }
124+
async-trait = "0.1.88"
125+
118126
[workspace.lints]
119127
rust.missing_debug_implementations = "warn"
120128
rust.unreachable_pub = "warn"

crates/client-executor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ workspace = true
1111
[dependencies]
1212
eyre.workspace = true
1313
serde.workspace = true
14+
sha2.workspace = true
1415
serde_with = "3.12.0"
1516
thiserror.workspace = true
1617

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use alloy_consensus::Header;
2+
use alloy_primitives::{B256, U256};
3+
use serde::{Deserialize, Serialize};
4+
use serde_with::serde_as;
5+
use sha2::{Digest, Sha256};
6+
7+
use crate::AnchorType;
8+
9+
/// The generalized Merkle tree index of the `block_hash` field in the `BeaconBlock`.
10+
pub const BLOCK_HASH_LEAF_INDEX: usize = 6444;
11+
12+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13+
pub enum Anchor {
14+
Header(HeaderAnchor),
15+
Beacon(BeaconAnchor),
16+
}
17+
18+
impl Anchor {
19+
pub fn header(&self) -> &Header {
20+
match self {
21+
Anchor::Header(header_anchor) => &header_anchor.header,
22+
Anchor::Beacon(beacon_anchor) => &beacon_anchor.inner.header,
23+
}
24+
}
25+
26+
pub fn id(&self) -> U256 {
27+
match self {
28+
Anchor::Header(header_anchor) => U256::from(header_anchor.header.number),
29+
Anchor::Beacon(beacon_anchor) => U256::from(beacon_anchor.timestamp),
30+
}
31+
}
32+
33+
pub fn hash(&self) -> B256 {
34+
match self {
35+
Anchor::Header(header_anchor) => header_anchor.header.hash_slow(),
36+
Anchor::Beacon(beacon_anchor) => {
37+
let block_hash = beacon_anchor.inner.header.hash_slow();
38+
39+
rebuild_merkle_root(block_hash, BLOCK_HASH_LEAF_INDEX, &beacon_anchor.proof)
40+
}
41+
}
42+
}
43+
44+
pub fn ty(&self) -> AnchorType {
45+
match self {
46+
Anchor::Header(_) => AnchorType::BlockHash,
47+
Anchor::Beacon(_) => AnchorType::BeaconRoot,
48+
}
49+
}
50+
}
51+
52+
impl From<Header> for Anchor {
53+
fn from(header: Header) -> Self {
54+
Self::Header(HeaderAnchor { header })
55+
}
56+
}
57+
58+
#[serde_as]
59+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60+
pub struct HeaderAnchor {
61+
#[serde_as(as = "alloy_consensus::serde_bincode_compat::Header")]
62+
header: Header,
63+
}
64+
65+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66+
pub struct BeaconAnchor {
67+
inner: HeaderAnchor,
68+
proof: Vec<B256>,
69+
timestamp: u64,
70+
}
71+
72+
impl BeaconAnchor {
73+
pub fn new(header: Header, proof: Vec<B256>, timestamp: u64) -> Self {
74+
Self { inner: HeaderAnchor { header }, proof, timestamp }
75+
}
76+
}
77+
78+
pub fn rebuild_merkle_root(leaf: B256, generalized_index: usize, branch: &[B256]) -> B256 {
79+
let mut current_hash = leaf;
80+
let depth = generalized_index.ilog2();
81+
let mut index = generalized_index - (1 << depth);
82+
let mut hasher = Sha256::new();
83+
84+
for sibling in branch {
85+
// Determine if the current node is a left or right child
86+
let is_left = index % 2 == 0;
87+
88+
// Combine the current hash with the sibling hash
89+
if is_left {
90+
// If current node is left child, hash(current + sibling)
91+
hasher.update(current_hash);
92+
hasher.update(sibling);
93+
} else {
94+
// If current node is right child, hash(sibling + current)
95+
hasher.update(sibling);
96+
hasher.update(current_hash);
97+
}
98+
current_hash.copy_from_slice(&hasher.finalize_reset());
99+
100+
// Move up to the parent level
101+
index /= 2;
102+
}
103+
104+
current_hash
105+
}

crates/client-executor/src/io.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ use rsp_primitives::genesis::Genesis;
1010
use serde::{Deserialize, Serialize};
1111
use serde_with::serde_as;
1212

13+
use crate::Anchor;
14+
1315
/// Information about how the contract executions accessed state, which is needed to execute the
1416
/// contract in SP1.
1517
///
1618
/// Instead of passing in the entire state, only the state roots and merkle proofs
1719
/// for the storage slots that were modified and accessed are passed in.
1820
#[serde_as]
1921
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20-
pub struct EVMStateSketch {
22+
pub struct EvmSketchInput {
23+
/// The current block anchor.
24+
pub anchor: Anchor,
2125
/// The genesis block specification.
2226
pub genesis: Genesis,
23-
/// The current block header.
24-
#[serde_as(as = "alloy_consensus::serde_bincode_compat::Header")]
25-
pub header: Header,
2627
/// The previous block headers starting from the most recent. These are used for calls to the
2728
/// blockhash opcode.
2829
#[serde_as(as = "Vec<alloy_consensus::serde_bincode_compat::Header>")]
@@ -34,19 +35,19 @@ pub struct EVMStateSketch {
3435
/// Account bytecodes.
3536
pub bytecodes: Vec<Bytecode>,
3637
/// Receipts.
37-
#[serde_as(as = "Vec<alloy_consensus::serde_bincode_compat::ReceiptEnvelope>")]
38-
pub receipts: Vec<ReceiptEnvelope>,
38+
#[serde_as(as = "Option<Vec<alloy_consensus::serde_bincode_compat::ReceiptEnvelope>>")]
39+
pub receipts: Option<Vec<ReceiptEnvelope>>,
3940
}
4041

41-
impl WitnessInput for EVMStateSketch {
42+
impl WitnessInput for EvmSketchInput {
4243
#[inline(always)]
4344
fn state(&self) -> &EthereumState {
4445
&self.state
4546
}
4647

4748
#[inline(always)]
4849
fn state_anchor(&self) -> B256 {
49-
self.header.state_root
50+
self.anchor.header().state_root
5051
}
5152

5253
#[inline(always)]
@@ -61,6 +62,6 @@ impl WitnessInput for EVMStateSketch {
6162

6263
#[inline(always)]
6364
fn headers(&self) -> impl Iterator<Item = &Header> {
64-
once(&self.header).chain(self.ancestor_headers.iter())
65+
once(self.anchor.header()).chain(self.ancestor_headers.iter())
6566
}
6667
}

crates/client-executor/src/lib.rs

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use alloy_rpc_types::{Filter, FilteredParams};
88
use alloy_sol_types::{sol, SolCall, SolEvent};
99
use alloy_trie::root::ordered_trie_root_with_encoder;
1010
use eyre::OptionExt;
11-
use io::EVMStateSketch;
11+
use io::EvmSketchInput;
1212
use reth_chainspec::ChainSpec;
1313
use reth_evm::{ConfigureEvm, EvmEnv};
1414
use reth_evm_ethereum::{EthEvm, EthEvmConfig};
@@ -20,6 +20,9 @@ use revm_primitives::{Address, Bytes, TxKind, B256, U256};
2020
use rsp_client_executor::io::{TrieDB, WitnessInput};
2121
use rsp_primitives::genesis::Genesis;
2222

23+
mod anchor;
24+
pub use anchor::{rebuild_merkle_root, Anchor, BeaconAnchor, HeaderAnchor, BLOCK_HASH_LEAF_INDEX};
25+
2326
mod errors;
2427
pub use errors::ClientError;
2528

@@ -100,11 +103,17 @@ impl IntoTxEnv<TxEnv> for &ContractInput {
100103
}
101104

102105
sol! {
106+
#[derive(Debug)]
107+
enum AnchorType { BlockHash, BeaconRoot }
108+
103109
/// Public values of a contract call.
104110
///
105111
/// These outputs can easily be abi-encoded, for use on-chain.
112+
#[derive(Debug)]
106113
struct ContractPublicValues {
107-
bytes32 blockHash;
114+
uint256 id;
115+
bytes32 anchorHash;
116+
AnchorType anchorType;
108117
address callerAddress;
109118
address contractAddress;
110119
bytes contractCalldata;
@@ -117,46 +126,65 @@ impl ContractPublicValues {
117126
///
118127
/// By default, commit the contract input, the output, and the block hash to public values of
119128
/// the proof. More can be committed if necessary.
120-
pub fn new(call: ContractInput, output: Bytes, block_hash: B256) -> Self {
129+
pub fn new(
130+
call: ContractInput,
131+
output: Bytes,
132+
id: U256,
133+
anchor: B256,
134+
anchor_type: AnchorType,
135+
) -> Self {
121136
Self {
137+
id,
138+
anchorHash: anchor,
139+
anchorType: anchor_type,
122140
contractAddress: call.contract_address,
123141
callerAddress: call.caller_address,
124142
contractCalldata: call.calldata.to_bytes(),
125143
contractOutput: output,
126-
blockHash: block_hash,
127144
}
128145
}
129146
}
130147

131148
/// An executor that executes smart contract calls inside a zkVM.
132149
#[derive(Debug)]
133150
pub struct ClientExecutor<'a> {
151+
/// The block anchor.
152+
pub anchor: &'a Anchor,
134153
/// The genesis block specification.
135154
pub genesis: &'a Genesis,
136155
/// The database that the executor uses to access state.
137156
pub witness_db: TrieDB<'a>,
138-
/// The block header.
139-
pub header: &'a Header,
140157
/// All logs in the block.
141158
pub logs: Vec<Log>,
142159
}
143160

144161
impl<'a> ClientExecutor<'a> {
145162
/// Instantiates a new [`ClientExecutor`]
146-
pub fn new(state_sketch: &'a EVMStateSketch) -> Result<Self, ClientError> {
147-
if !state_sketch.receipts.is_empty() {
163+
pub fn new(state_sketch: &'a EvmSketchInput) -> Result<Self, ClientError> {
164+
assert_eq!(
165+
state_sketch.anchor.header().state_root,
166+
state_sketch.state.state_root(),
167+
"State root mismatch"
168+
);
169+
170+
if let Some(receipts) = &state_sketch.receipts {
148171
// verify the receipts root hash
149-
let root =
150-
ordered_trie_root_with_encoder(&state_sketch.receipts, |r, out| r.encode_2718(out));
151-
assert_eq!(state_sketch.header.receipts_root, root, "Receipts root mismatch");
172+
let root = ordered_trie_root_with_encoder(receipts, |r, out| r.encode_2718(out));
173+
assert_eq!(state_sketch.anchor.header().receipts_root, root, "Receipts root mismatch");
152174
}
153175

154-
let logs = state_sketch.receipts.iter().flat_map(|r| r.logs().to_vec()).collect();
176+
let logs = state_sketch
177+
.receipts
178+
.as_ref()
179+
.unwrap_or(&vec![])
180+
.iter()
181+
.flat_map(|r| r.logs().to_vec())
182+
.collect();
155183

156184
Ok(Self {
185+
anchor: &state_sketch.anchor,
157186
genesis: &state_sketch.genesis,
158187
witness_db: state_sketch.witness_db()?,
159-
header: &state_sketch.header,
160188
logs,
161189
})
162190
}
@@ -166,10 +194,19 @@ impl<'a> ClientExecutor<'a> {
166194
/// Storage accesses are already validated against the `witness_db`'s state root.
167195
pub fn execute(&self, call: ContractInput) -> eyre::Result<ContractPublicValues> {
168196
let cache_db = CacheDB::new(&self.witness_db);
169-
let mut evm = new_evm(cache_db, self.header, U256::ZERO, self.genesis);
197+
let mut evm = new_evm(cache_db, self.anchor.header(), U256::ZERO, self.genesis);
170198
let tx_output = evm.transact(&call)?;
171199
let tx_output_bytes = tx_output.result.output().ok_or_eyre("Error decoding result")?;
172-
Ok(ContractPublicValues::new(call, tx_output_bytes.clone(), self.header.hash_slow()))
200+
201+
let public_values = ContractPublicValues::new(
202+
call,
203+
tx_output_bytes.clone(),
204+
self.anchor.id(),
205+
self.anchor.hash(),
206+
self.anchor.ty(),
207+
);
208+
209+
Ok(public_values)
173210
}
174211

175212
/// Returns the decoded logs matching the provided `filter`.

crates/host-executor/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ license.workspace = true
99
workspace = true
1010

1111
[dependencies]
12+
async-trait.workspace = true
1213
eyre.workspace = true
14+
reqwest.workspace = true
15+
serde.workspace = true
1316
url.workspace = true
1417
tokio.workspace = true
1518
tracing.workspace = true
@@ -38,8 +41,10 @@ alloy-sol-types.workspace = true
3841
alloy-rpc-types.workspace = true
3942
alloy-evm.workspace = true
4043

44+
ethereum-consensus.workspace = true
45+
4146
[dev-dependencies]
4247
alloy-primitives.workspace = true
48+
dotenv.workspace = true
4349
tracing-subscriber = "0.3.18"
4450
bincode = "1.3.3"
45-
dotenv.workspace = true

0 commit comments

Comments
 (0)