Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #503: handle empty account or storage value in proof verification #508

Merged
merged 2 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading