risc0_steel/account.rs
1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Types related to account queries.
16use crate::{EvmFactory, GuestEvmEnv, StateAccount};
17use alloy_primitives::{Address, Bytes, B256, U256};
18use anyhow::Result;
19
20/// Information about an Ethereum account.
21///
22/// This struct contains all the essential data that makes up an Ethereum account's state,
23/// including its balance, nonce, storage, and code information.
24#[derive(Debug, Clone, Eq)]
25pub struct AccountInfo {
26 /// The number of transactions sent from this account (also used as replay protection).
27 pub nonce: u64,
28 /// The account's current balance in Wei.
29 pub balance: U256,
30 /// The Keccak-256 hash of the root node of the account's storage trie.
31 pub storage_root: B256,
32 /// The Keccak-256 hash of the account's code.
33 /// For non-contract accounts (EOAs), this will be the hash of empty bytes.
34 pub code_hash: B256,
35 /// The actual bytecode of the account.
36 /// This is `None` when the code hasn't been loaded.
37 pub code: Option<Bytes>,
38}
39
40impl PartialEq for AccountInfo {
41 fn eq(&self, other: &Self) -> bool {
42 self.nonce == other.nonce
43 && self.balance == other.balance
44 && self.storage_root == other.storage_root
45 && self.code_hash == other.code_hash
46 }
47}
48
49impl From<StateAccount> for AccountInfo {
50 fn from(account: StateAccount) -> Self {
51 Self {
52 nonce: account.nonce,
53 balance: account.balance,
54 storage_root: account.storage_root,
55 code_hash: account.code_hash,
56 code: None,
57 }
58 }
59}
60
61/// Represents an EVM account query.
62///
63/// ### Usage
64/// - **Preflight calls on the Host:** To prepare the account query on the host environment and
65/// build the necessary proof, use [Account::preflight].
66/// - **Calls in the Guest:** To initialize the account query in the guest, use [Account::new].
67///
68/// ### Examples
69/// ```rust,no_run
70/// # use risc0_steel::{Account, ethereum::{ETH_MAINNET_CHAIN_SPEC, EthEvmEnv}};
71/// # use alloy_primitives::address;
72/// # #[tokio::main(flavor = "current_thread")]
73/// # async fn main() -> anyhow::Result<()> {
74/// let account_address = address!("F977814e90dA44bFA03b6295A0616a897441aceC");
75///
76/// // Host:
77/// let url = "https://ethereum-rpc.publicnode.com".parse()?;
78/// let mut env = EthEvmEnv::builder().rpc(url).chain_spec(Ð_MAINNET_CHAIN_SPEC).build().await?;
79/// let account = Account::preflight(account_address, &mut env);
80/// let info = account.bytecode(true).info().await?;
81///
82/// let evm_input = env.into_input().await?;
83///
84/// // Guest:
85/// let env = evm_input.into_env(Ð_MAINNET_CHAIN_SPEC);
86/// let account = Account::new(account_address, &env);
87/// let info = account.bytecode(true).info();
88///
89/// # Ok(())
90/// # }
91/// ```
92pub struct Account<E> {
93 address: Address,
94 env: E,
95 code: bool,
96}
97
98impl<E> Account<E> {
99 /// Sets whether to fetch the bytecode for this account.
100 ///
101 /// If set to `true`, the bytecode will be fetched when calling [Account::info].
102 pub fn bytecode(mut self, code: bool) -> Self {
103 self.code = code;
104 self
105 }
106}
107
108impl<'a, F: EvmFactory> Account<&'a GuestEvmEnv<F>> {
109 /// Constructor for querying an Ethereum account in the guest.
110 pub fn new(address: Address, env: &'a GuestEvmEnv<F>) -> Self {
111 Self {
112 address,
113 env,
114 code: false,
115 }
116 }
117
118 /// Attempts to get the [AccountInfo] for the corresponding account and returns an error if the
119 /// query fails.
120 ///
121 /// In general, it's recommended to use [Account::info] unless explicit error handling is
122 /// required.
123 pub fn try_info(self) -> Result<AccountInfo> {
124 let db = self.env.db();
125 let account = db.account(self.address).unwrap_or_default();
126 let mut info = AccountInfo::from(account);
127 if self.code && info.code.is_none() {
128 let code = db.code_by_hash(info.code_hash);
129 info.code = Some(code.clone());
130 }
131
132 Ok(info)
133 }
134
135 /// Gets the [AccountInfo] for the corresponding account and panics on failure.
136 ///
137 /// A convenience wrapper for [Account::try_info], panicking if the query fails. Useful when
138 /// success is expected.
139 pub fn info(self) -> AccountInfo {
140 self.try_info().unwrap()
141 }
142}
143
144#[cfg(feature = "host")]
145mod host {
146 use super::*;
147 use crate::host::{db::ProviderDb, HostEvmEnv};
148 use alloy::{network::Network, providers::Provider};
149 use anyhow::{ensure, Context};
150 use revm::Database as RevmDatabase;
151
152 impl<'a, N, P, F, C> Account<&'a mut HostEvmEnv<ProviderDb<N, P>, F, C>>
153 where
154 N: Network,
155 P: Provider<N>,
156 ProviderDb<N, P>: Send + 'static,
157 F: EvmFactory,
158 {
159 /// Constructor for preflighting queries to an Ethereum account on the host.
160 ///
161 /// Initializes the environment for querying account information, fetching necessary data
162 /// via the [Provider], and generating a storage proof for any accessed elements using
163 /// [EvmEnv::into_input].
164 ///
165 /// [EvmEnv::into_input]: crate::EvmEnv::into_input
166 /// [EvmEnv]: crate::EvmEnv
167 pub fn preflight(
168 address: Address,
169 env: &'a mut HostEvmEnv<ProviderDb<N, P>, F, C>,
170 ) -> Self {
171 Self {
172 address,
173 env,
174 code: false,
175 }
176 }
177
178 /// Gets the [AccountInfo] for the corresponding account using an [EvmEnv] constructed with
179 /// [Account::preflight].
180 ///
181 /// [EvmEnv]: crate::EvmEnv
182 pub async fn info(self) -> Result<AccountInfo> {
183 log::info!("Executing preflight querying account {}", &self.address);
184
185 let account = self
186 .env
187 .db_mut()
188 .state_account(self.address)
189 .await
190 .context("failed to get state account information")?;
191 let mut info = AccountInfo::from(account);
192 if self.code && info.code.is_none() {
193 // basic must always be called first
194 let basic = self
195 .env
196 .spawn_with_db(move |db| db.basic(self.address))
197 .await
198 .context("failed to get basic account information")?
199 .unwrap_or_default();
200 ensure!(basic.code_hash == account.code_hash, "code_hash mismatch");
201
202 let code = self
203 .env
204 .spawn_with_db(move |db| db.code_by_hash(info.code_hash))
205 .await
206 .context("failed to get account code by its hash")?;
207 info.code = Some(code.original_bytes());
208 }
209
210 Ok(info)
211 }
212 }
213}