Skip to content

Commit

Permalink
fix: handle empty account or storage value in proof verification (#508)
Browse files Browse the repository at this point in the history
* handle empty account or storage value in proof verification

* fixup
  • Loading branch information
eshaan7 authored Feb 7, 2025
1 parent 6224e82 commit 19b841a
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 80 deletions.
46 changes: 5 additions & 41 deletions core/src/execution/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ use alloy::network::{BlockResponse, ReceiptResponse};
use alloy::primitives::{keccak256, Address, B256, U256};
use alloy::rlp;
use alloy::rpc::types::{BlockTransactions, Filter, FilterChanges, Log};
use alloy_trie::{
proof::verify_proof as mpt_verify_proof,
root::ordered_trie_root_with_encoder,
{Nibbles, TrieAccount},
};
use alloy_trie::root::ordered_trie_root_with_encoder;
use constants::{BLOB_BASE_FEE_UPDATE_FRACTION, MIN_BASE_FEE_PER_BLOB_GAS};
use eyre::Result;
use futures::future::try_join_all;
use proof::{verify_account_proof, verify_storage_proof};
use revm::primitives::KECCAK_EMPTY;
use tracing::warn;

Expand All @@ -29,6 +26,7 @@ use self::types::Account;
pub mod constants;
pub mod errors;
pub mod evm;
pub mod proof;
pub mod rpc;
pub mod state;
pub mod types;
Expand Down Expand Up @@ -72,43 +70,9 @@ impl<N: NetworkSpec, R: ExecutionRpc<N>> ExecutionClient<N, R> {
.await?;

// Verify the account proof
let account_key = Nibbles::unpack(keccak256(proof.address));
let account = TrieAccount {
nonce: proof.nonce,
balance: proof.balance,
storage_root: proof.storage_hash,
code_hash: proof.code_hash,
};
let account_encoded = rlp::encode(account);

mpt_verify_proof(
block.header().state_root(),
account_key,
account_encoded.into(),
&proof.account_proof,
)
.map_err(|_| ExecutionError::InvalidAccountProof(address))?;

verify_account_proof(&proof, block.header().state_root())?;
// Verify the storage proofs, collecting the slot values
let mut slot_map = HashMap::new();

for storage_proof in proof.storage_proof {
let key = storage_proof.key.as_b256();
let key_hash = keccak256(key);
let key_nibbles = Nibbles::unpack(key_hash);
let encoded_value = rlp::encode(storage_proof.value);

mpt_verify_proof(
proof.storage_hash,
key_nibbles,
encoded_value.into(),
&storage_proof.proof,
)
.map_err(|_| ExecutionError::InvalidStorageProof(address, key))?;

slot_map.insert(key, storage_proof.value);
}

let slot_map = verify_storage_proof(&proof)?;
// Verify the code hash
let code = if proof.code_hash == KECCAK_EMPTY || proof.code_hash == B256::ZERO {
Vec::new()
Expand Down
89 changes: 89 additions & 0 deletions core/src/execution/proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::collections::HashMap;

use alloy::primitives::{keccak256, Bytes, B256, U256};
use alloy::rlp;
use alloy::rpc::types::EIP1186AccountProofResponse;
use alloy_trie::{
proof::verify_proof,
{Nibbles, TrieAccount},
};
use eyre::{eyre, Result};

use super::errors::ExecutionError;

/// Verify a given `EIP1186AccountProofResponse`'s account proof against given state root.
pub fn verify_account_proof(proof: &EIP1186AccountProofResponse, state_root: B256) -> Result<()> {
let account_key = proof.address;
let account = TrieAccount {
nonce: proof.nonce,
balance: proof.balance,
storage_root: proof.storage_hash,
code_hash: proof.code_hash,
};

verify_mpt_proof(state_root, account_key, account, &proof.account_proof)
.map_err(|_| eyre!(ExecutionError::InvalidAccountProof(proof.address)))
}

/// Verify a given `EIP1186AccountProofResponse`'s storage proof against the storage root.
/// Also returns a map of storage slots.
pub fn verify_storage_proof(proof: &EIP1186AccountProofResponse) -> Result<HashMap<B256, U256>> {
let mut slot_map = HashMap::with_capacity(proof.storage_proof.len());

for storage_proof in &proof.storage_proof {
let key = storage_proof.key.as_b256();
let value = storage_proof.value;

verify_mpt_proof(proof.storage_hash, key, value, &storage_proof.proof)
.map_err(|_| ExecutionError::InvalidStorageProof(proof.address, key))?;

slot_map.insert(key, value);
}

Ok(slot_map)
}

/// Verifies a MPT proof for a given key-value pair against the provided root hash.
/// This function wraps `alloy_trie::proof::verify_proof` and checks
/// if the value represents an empty account or slot to support exclusion proofs.
///
/// # Parameters
/// - `root`: The root hash of the MPT.
/// - `raw_key`: The key to be verified, which will be hashed using `keccak256`.
/// - `raw_value`: The value associated with the key, which will be RLP encoded.
/// - `proof`: A slice of bytes representing the MPT proof.
pub fn verify_mpt_proof<K: AsRef<[u8]>, V: rlp::Encodable>(
root: B256,
raw_key: K,
raw_value: V,
proof: &[Bytes],
) -> Result<()> {
let key = Nibbles::unpack(keccak256(raw_key));
let value = rlp::encode(raw_value);

let value = if is_empty_value(&value) {
None // exclusion proof
} else {
Some(value) // inclusion proof
};

verify_proof(root, key, value, proof).map_err(|e| eyre!(e))
}

/// Check if the value is an empty account or empty slot.
fn is_empty_value(value: &[u8]) -> bool {
let empty_account = TrieAccount::default();
let new_empty_account = TrieAccount {
nonce: 0,
balance: U256::ZERO,
storage_root: B256::ZERO,
code_hash: B256::ZERO,
};

let empty_account = rlp::encode(empty_account);
let new_empty_account = rlp::encode(new_empty_account);

let is_empty_slot = value.len() == 1 && value[0] == 0x80;
let is_empty_account = value == empty_account || value == new_empty_account;
is_empty_slot || is_empty_account
}
55 changes: 16 additions & 39 deletions opstack/src/consensus.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use alloy::consensus::proofs::{calculate_transaction_root, calculate_withdrawals_root};
use alloy::consensus::{Header as ConsensusHeader, Transaction as TxTrait};
use alloy::eips::eip4895::{Withdrawal, Withdrawals};
use alloy::primitives::{b256, fixed_bytes, keccak256, Address, Bloom, BloomInput, B256, U256};
use alloy::rlp::{self, Decodable};
use alloy::primitives::{b256, fixed_bytes, Address, Bloom, BloomInput, B256, U256};
use alloy::rlp::Decodable;
use alloy::rpc::types::{
Block, EIP1186AccountProofResponse, Header, Transaction as EthTransaction,
};
use alloy_trie::{
proof::verify_proof as mpt_verify_proof,
{Nibbles, TrieAccount},
};
use eyre::{eyre, OptionExt, Result};
use op_alloy_consensus::OpTxEnvelope;
use op_alloy_network::primitives::BlockTransactions;
use op_alloy_rpc_types::Transaction;
use std::str::FromStr;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
use tokio::sync::{
mpsc::{channel, Receiver},
watch,
};
use tracing::{error, info, warn};

use helios_consensus_core::consensus_spec::MainnetConsensusSpec;
use helios_core::consensus::Consensus;
use helios_core::execution::proof::{verify_account_proof, verify_mpt_proof};
use helios_core::time::{interval, SystemTime, UNIX_EPOCH};
use helios_ethereum::consensus::ConsensusClient as EthConsensusClient;
use std::sync::{Arc, Mutex};

use crate::{config::Config, types::ExecutionPayload, SequencerCommitment};

use helios_ethereum::database::ConfigDB;
use helios_ethereum::rpc::http_rpc::HttpRpc;
use tracing::{error, info, warn};

use crate::{config::Config, types::ExecutionPayload, SequencerCommitment};

// Storage slot containing the unsafe signer address in all superchain system config contracts
const UNSAFE_SIGNER_SLOT: &str =
Expand Down Expand Up @@ -208,24 +206,7 @@ fn verify_unsafe_signer(config: Config, signer: Arc<Mutex<Address>>) {

// Verify unsafe signer
// with account proof
let account_key = Nibbles::unpack(keccak256(proof.address));
let account = TrieAccount {
nonce: proof.nonce,
balance: proof.balance,
storage_root: proof.storage_hash,
code_hash: proof.code_hash,
};
let account_encoded = rlp::encode(account);

let is_valid = mpt_verify_proof(
block.header.state_root,
account_key,
account_encoded.into(),
&proof.account_proof,
)
.is_ok();

if !is_valid {
if verify_account_proof(&proof, block.header.state_root).is_err() {
warn!(target: "helios::opstack", "account proof invalid");
return Err(eyre!("account proof invalid"));
}
Expand All @@ -238,18 +219,14 @@ fn verify_unsafe_signer(config: Config, signer: Arc<Mutex<Address>>) {
return Err(eyre!("account proof invalid"));
}

let key_hash = keccak256(key);
let key_nibbles = Nibbles::unpack(key_hash);
let encoded_value = rlp::encode(storage_proof.value);
let is_valid = mpt_verify_proof(
if verify_mpt_proof(
proof.storage_hash,
key_nibbles,
encoded_value.into(),
key,
storage_proof.value,
&storage_proof.proof,
)
.is_ok();

if !is_valid {
.is_err()
{
warn!(target: "helios::opstack", "storage proof invalid");
return Err(eyre!("storage proof invalid"));
}
Expand Down

0 comments on commit 19b841a

Please sign in to comment.