Skip to content

Commit 0b92abe

Browse files
authored
feat: small performance optimizations (#493)
* perf: improve allocs - reserve a bigger vector for trie nodes from the beginning. remove capacity increase in inserts. - use an uninit array for branch childs during decoding * perf: decode keccak alignment Encode RLP representation of each node with extra padding to make them word-aligned. This let us to avoid memcpy operations when calculating `keccak(rlp_encoded)` during decoding. * feat: improve witness db - Revm's `CacheDB` already caches state's keys and values. We remove the duplicate caching in witness db by simplifying it. - Add a keccak cache of address and storage slots. With this we avoid hashing some addresses and storage slots more than once. * fix: fmt * perf: remove keccak cache * chore: add `SAFETY` comments
1 parent cd587ce commit 0b92abe

File tree

12 files changed

+193
-208
lines changed

12 files changed

+193
-208
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ members = [
77
"crates/mpt-tools",
88
"crates/primitives",
99
"crates/storage/rpc-db",
10-
"crates/storage/witness-db",
1110
"crates/host-bench",
1211
"crates/mptnew",
1312
"bin/verifier-bench",
@@ -56,7 +55,6 @@ bitcode = { version = "0.6.5", default-features = false, features = ["serde"] }
5655

5756
# workspace
5857
openvm-rpc-db = { path = "./crates/storage/rpc-db" }
59-
openvm-witness-db = { path = "./crates/storage/witness-db" }
6058
openvm-client-executor = { path = "./crates/executor/client" }
6159
openvm-host-executor = { path = "./crates/executor/host" }
6260
openvm-mpt = { path = "./crates/mpt" }

bin/client-eth/Cargo.lock

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

crates/executor/client/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ bumpalo.workspace = true
1717
itertools = "0.14"
1818

1919
# workspace
20-
openvm-witness-db.workspace = true
2120
openvm-mpt.workspace = true
2221
openvm-primitives.workspace = true
2322
mptnew.workspace = true

crates/executor/client/src/io.rs

Lines changed: 92 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use std::iter::once;
22

33
use bumpalo::Bump;
4-
use eyre::{bail, OptionExt, Result};
4+
use eyre::{bail, Result};
55
use itertools::Itertools;
6-
use openvm_witness_db::WitnessDb;
6+
use reth_evm::execute::ProviderError;
77
use reth_primitives::{Block, Header, TransactionSigned};
88
use reth_trie::TrieAccount;
9-
use revm::state::{AccountInfo, Bytecode};
9+
use revm::{
10+
state::{AccountInfo, Bytecode},
11+
DatabaseRef,
12+
};
1013
use revm_primitives::{keccak256, map::DefaultHashBuilder, Address, HashMap, B256, U256};
1114
use serde::{Deserialize, Serialize};
1215
use serde_with::serde_as;
@@ -30,8 +33,6 @@ pub struct ClientExecutorInput {
3033
pub ancestor_headers: Vec<Header>,
3134
/// Network state as of the parent block.
3235
pub parent_state_bytes: mptnew::EthereumStateBytes,
33-
/// Requests to account state and storage slots.
34-
pub state_requests: HashMap<Address, Vec<U256>>,
3536
/// Account bytecodes.
3637
pub bytecodes: Vec<Bytecode>,
3738
}
@@ -65,7 +66,7 @@ impl ClientExecutorInputWithState {
6566
&input.parent_state_bytes.storage_tries
6667
{
6768
let account_in_trie =
68-
state_trie.get_rlp::<TrieAccount>(hashed_address.as_slice()).unwrap();
69+
state_trie.get_rlp::<TrieAccount>(hashed_address.as_slice())?;
6970
let expected_storage_root =
7071
account_in_trie.map_or(reth_trie::EMPTY_ROOT_HASH, |a| a.storage_root);
7172

@@ -80,6 +81,7 @@ impl ClientExecutorInputWithState {
8081

8182
storage_tries.insert(*hashed_address, storage_trie);
8283
}
84+
8385
mptnew::EthereumState { state_trie, storage_tries, bump }
8486
};
8587

@@ -95,7 +97,7 @@ impl ClientExecutorInputWithState {
9597
}
9698

9799
/// Creates a [`WitnessDb`].
98-
pub fn witness_db(&self) -> Result<WitnessDb> {
100+
pub fn witness_db(&self) -> Result<WitnessDb<'_>> {
99101
<Self as WitnessInput>::witness_db(self)
100102
}
101103
}
@@ -111,11 +113,6 @@ impl WitnessInput for ClientExecutorInputWithState {
111113
self.parent_header().state_root
112114
}
113115

114-
#[inline(always)]
115-
fn state_requests(&self) -> impl Iterator<Item = (&Address, &Vec<U256>)> {
116-
self.input.state_requests.iter()
117-
}
118-
119116
#[inline(always)]
120117
fn bytecodes(&self) -> impl Iterator<Item = &Bytecode> {
121118
self.input.bytecodes.iter()
@@ -125,6 +122,11 @@ impl WitnessInput for ClientExecutorInputWithState {
125122
fn headers(&self) -> impl Iterator<Item = &Header> {
126123
once(&self.input.current_block.header).chain(self.input.ancestor_headers.iter())
127124
}
125+
126+
#[inline(always)]
127+
fn headers_len(&self) -> usize {
128+
1 + self.input.ancestor_headers.len()
129+
}
128130
}
129131

130132
/// A trait for constructing [`WitnessDb`].
@@ -136,18 +138,16 @@ pub trait WitnessInput {
136138
/// [state()](trait.WitnessInput#tymethod.state) must conform to.
137139
fn state_anchor(&self) -> B256;
138140

139-
/// Gets an iterator over address state requests. For each request, the account info and storage
140-
/// slots are loaded from the relevant tries in the state returned by
141-
/// [state()](trait.WitnessInput#tymethod.state).
142-
fn state_requests(&self) -> impl Iterator<Item = (&Address, &Vec<U256>)>;
143-
144141
/// Gets an iterator over account bytecodes.
145142
fn bytecodes(&self) -> impl Iterator<Item = &Bytecode>;
146143

147144
/// Gets an iterator over references to a consecutive, reverse-chronological block headers
148145
/// starting from the current block header.
149146
fn headers(&self) -> impl Iterator<Item = &Header>;
150147

148+
/// Gets the number of headers.
149+
fn headers_len(&self) -> usize;
150+
151151
/// Creates a [`WitnessDb`] from a [`WitnessInput`] implementation. To do so, it verifies the
152152
/// state root, ancestor headers and account bytecodes, and constructs the account and
153153
/// storage values by reading against state tries.
@@ -156,68 +156,16 @@ pub trait WitnessInput {
156156
/// implementing this trait causes a zkVM run to cost over 5M cycles more. To avoid this, define
157157
/// a method inside the type that calls this trait method instead.
158158
#[inline(always)]
159-
fn witness_db(&self) -> Result<WitnessDb> {
159+
fn witness_db(&self) -> Result<WitnessDb<'_>> {
160160
let state = self.state();
161161

162-
let bytecodes_by_hash =
162+
let bytecode_by_hash =
163163
self.bytecodes().map(|code| (code.hash_slow(), code)).collect::<HashMap<_, _>>();
164164

165-
let state_requests_iter = self.state_requests();
166-
let (lower, _) = state_requests_iter.size_hint();
167-
let mut accounts = HashMap::with_capacity_and_hasher(lower, DefaultHashBuilder::default());
168-
let mut storage = HashMap::with_capacity_and_hasher(lower, DefaultHashBuilder::default());
169-
170-
for (&address, slots) in state_requests_iter {
171-
let hashed_address = keccak256(address);
172-
173-
let account_in_trie =
174-
state.state_trie.get_rlp::<TrieAccount>(hashed_address.as_slice())?;
175-
176-
accounts.insert(
177-
address,
178-
match account_in_trie {
179-
Some(account_in_trie) => AccountInfo {
180-
balance: account_in_trie.balance,
181-
nonce: account_in_trie.nonce,
182-
code_hash: account_in_trie.code_hash,
183-
code: Some(
184-
(*bytecodes_by_hash
185-
.get(&account_in_trie.code_hash)
186-
.ok_or_eyre("missing bytecode")?)
187-
// Cloning here is fine as `Bytes` is cheap to clone.
188-
.to_owned(),
189-
),
190-
},
191-
_ => Default::default(),
192-
},
193-
);
194-
195-
if !slots.is_empty() {
196-
let mut address_storage =
197-
HashMap::with_capacity_and_hasher(slots.len(), DefaultHashBuilder::default());
198-
199-
let storage_trie = state
200-
.storage_tries
201-
.get(&hashed_address)
202-
.ok_or_eyre("parent state does not contain storage trie")?;
203-
204-
for &slot in slots {
205-
let slot_value = storage_trie
206-
.get_rlp::<U256>(keccak256(slot.to_be_bytes::<32>()).as_slice())?
207-
.unwrap_or_default();
208-
address_storage.insert(slot, slot_value);
209-
}
210-
211-
storage.insert(address, address_storage);
212-
}
213-
}
214-
215165
// Verify and build block hashes
216-
let headers_iter = self.headers();
217-
let (lower, _) = headers_iter.size_hint();
218166
let mut block_hashes: HashMap<u64, B256, _> =
219-
HashMap::with_capacity_and_hasher(lower, DefaultHashBuilder::default());
220-
for (child_header, parent_header) in headers_iter.tuple_windows() {
167+
HashMap::with_capacity_and_hasher(self.headers_len(), DefaultHashBuilder::default());
168+
for (child_header, parent_header) in self.headers().tuple_windows() {
221169
if parent_header.number != child_header.number - 1 {
222170
eyre::bail!("non-consecutive blocks");
223171
}
@@ -229,6 +177,76 @@ pub trait WitnessInput {
229177
block_hashes.insert(parent_header.number, child_header.parent_hash);
230178
}
231179

232-
Ok(WitnessDb { accounts, storage, block_hashes })
180+
Ok(WitnessDb { inner: state, block_hashes, bytecode_by_hash })
181+
}
182+
}
183+
184+
#[derive(Debug)]
185+
pub struct WitnessDb<'a> {
186+
inner: &'a mptnew::EthereumState,
187+
block_hashes: HashMap<u64, B256>,
188+
bytecode_by_hash: HashMap<B256, &'a Bytecode>,
189+
}
190+
191+
impl<'a> WitnessDb<'a> {
192+
pub fn new(
193+
inner: &'a mptnew::EthereumState,
194+
block_hashes: HashMap<u64, B256>,
195+
bytecode_by_hash: HashMap<B256, &'a Bytecode>,
196+
) -> Self {
197+
Self { inner, block_hashes, bytecode_by_hash }
198+
}
199+
}
200+
201+
impl DatabaseRef for WitnessDb<'_> {
202+
/// The database error type.
203+
type Error = ProviderError;
204+
205+
/// Get basic account information.
206+
fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
207+
let hashed_address = keccak256(address);
208+
209+
let account_in_trie =
210+
self.inner.state_trie.get_rlp::<TrieAccount>(hashed_address.as_slice()).unwrap();
211+
212+
let account = account_in_trie.map(|account_in_trie| AccountInfo {
213+
balance: account_in_trie.balance,
214+
nonce: account_in_trie.nonce,
215+
code_hash: account_in_trie.code_hash,
216+
code: None,
217+
});
218+
219+
Ok(account)
220+
}
221+
222+
/// Get account code by its hash.
223+
fn code_by_hash_ref(&self, hash: B256) -> Result<Bytecode, Self::Error> {
224+
// Cloning here is fine as `Bytes` is cheap to clone.
225+
Ok(self.bytecode_by_hash.get(&hash).map(|code| (*code).clone()).unwrap())
226+
}
227+
228+
/// Get storage value of address at index.
229+
fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
230+
let hashed_address = keccak256(address);
231+
232+
let storage_trie = self
233+
.inner
234+
.storage_tries
235+
.get(&hashed_address)
236+
.expect("A storage trie must be provided for each account");
237+
238+
let hashed_slot = keccak256(index.to_be_bytes::<32>());
239+
Ok(storage_trie
240+
.get_rlp::<U256>(hashed_slot.as_slice())
241+
.expect("Can get from MPT")
242+
.unwrap_or_default())
243+
}
244+
245+
/// Get block hash by block number.
246+
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
247+
Ok(*self
248+
.block_hashes
249+
.get(&number)
250+
.expect("A block hash must be provided for each block number"))
233251
}
234252
}

crates/executor/client/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ impl ClientExecutor {
103103
vec![executor_output.result.requests],
104104
);
105105

106+
drop(witness_db);
107+
106108
// Verify the state root.
107109
let state_root = profile!("compute state root", {
108110
input.state.update_from_bundle_state(&executor_outcome.bundle).unwrap();

crates/executor/host/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ impl<P: Provider<Ethereum> + Clone> HostExecutor<P> {
215215
current_block,
216216
ancestor_headers,
217217
parent_state_bytes: state_bytes,
218-
state_requests,
219218
bytecodes: rpc_db.get_bytecodes(),
220219
};
221220
tracing::info!("successfully generated client input");

crates/mpt/src/mpt.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -865,25 +865,26 @@ impl MptTrie<'_> {
865865
}
866866

867867
pub fn encode_trie(&self) -> Vec<u8> {
868-
let mut payload = Vec::new();
869-
self.encode_trie_internal(self.root_id, &mut payload);
870-
871868
let mut encoded = Vec::new();
872-
alloy_rlp::Header { list: true, payload_length: payload.len() }.encode(&mut encoded);
873-
encoded.append(&mut payload);
869+
self.encode_trie_internal(self.root_id, &mut encoded);
874870
encoded
875871
}
876872

877873
fn encode_trie_internal(&self, id: NodeId, out: &mut dyn alloy_rlp::BufMut) {
878874
let payload_length = self.payload_length_id(id);
879875
self.encode_id_with_payload_len(id, payload_length, out);
880876

877+
const MIN_ALIGN: usize = 4;
878+
let rlp_length = payload_length + alloy_rlp::length_of_length(payload_length);
879+
let padding_len = (MIN_ALIGN - (rlp_length % MIN_ALIGN)) % MIN_ALIGN;
880+
for _ in 0..padding_len {
881+
out.put_u8(0);
882+
}
883+
881884
match self.nodes[id as usize] {
882885
NodeData::Branch(childs) => childs.iter().for_each(|c| {
883886
if let Some(node) = c {
884887
self.encode_trie_internal(*node, out);
885-
} else {
886-
out.put_u8(alloy_rlp::EMPTY_STRING_CODE)
887888
}
888889
}),
889890
NodeData::Extension(_, node) => {

0 commit comments

Comments
 (0)