Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
804 changes: 416 additions & 388 deletions Cargo.lock

Large diffs are not rendered by default.

164 changes: 73 additions & 91 deletions Cargo.toml

Large diffs are not rendered by default.

81 changes: 54 additions & 27 deletions crates/node/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::{
time::Duration,
};

use alloy_consensus::BlockHeader;
use alloy_consensus::{BlockHeader, transaction::TxHashRef};
use alloy_eips::eip2935::{HISTORY_SERVE_WINDOW, HISTORY_STORAGE_ADDRESS};
use alloy_network::{ReceiptResponse, TransactionBuilder, TransactionResponse};
use alloy_primitives::{Address, B256, Bloom, Bytes, U64, U256, keccak256};
Expand All @@ -37,7 +37,7 @@ use reth_rpc_eth_api::{
};
use reth_rpc_eth_types::{EthApiError, logs_utils};
use reth_storage_api::{BlockNumReader, StateProviderFactory};
use reth_trie_common::{ExecutionWitnessMode, HashedStorage};
use reth_trie_common::{ExecutionWitnessMode, HashedPostState};
use tempo_alloy::{
TempoNetwork,
provider::ext::TempoProviderExt as _,
Expand Down Expand Up @@ -286,17 +286,27 @@ where
let (evm_config, recorder) = eth_api.evm_config().with_l1_storage_recorder();
let block_executor = evm_config.executor(&mut db);
let mode = ExecutionWitnessMode::default();
let mut witness_record = ExecutionWitnessRecord::default();
let mut witness = None;

let _ = block_executor
.execute_with_state_closure(&block, |statedb: &State<_>| {
witness_record.record_executed_state(statedb, mode);
record_block_hash_storage_proofs(&mut witness_record, statedb);
let mut additional_state = HashedPostState::default();
record_block_hash_storage_proofs(&mut additional_state, statedb);
witness = Some(
ExecutionWitnessRecord::new(statedb)
.with_additional_state(additional_state)
.into_execution_witness(
&statedb.database.database.0,
eth_api.provider(),
block_number,
mode,
),
);
})
.map_err(|error| EthApiError::Internal(error.into()))?;

let witness = witness_record
.into_execution_witness(&db.database.0, eth_api.provider(), block_number, mode)
let witness = witness
.expect("state closure is called after successful execution")
.map_err(EthApiError::from)?;
Ok(ZoneExecutionWitness {
execution_witness: witness,
Expand All @@ -320,17 +330,16 @@ where
/// Reth records these reads in REVM's block-hash cache and normally proves them with ancestor
/// headers. Zones already commit the EIP-2935 history contract in state, so adding the matching
/// storage targets lets the SPF authenticate the same values against the parent state root.
fn record_block_hash_storage_proofs<DB>(witness: &mut ExecutionWitnessRecord, state: &State<DB>) {
fn record_block_hash_storage_proofs<DB>(additional_state: &mut HashedPostState, state: &State<DB>) {
let block_hashes = state.block_hashes.iter().collect::<Vec<_>>();
if block_hashes.is_empty() {
return;
}

let history_storage = witness
.hashed_state
let history_storage = additional_state
.storages
.entry(keccak256(HISTORY_STORAGE_ADDRESS))
.or_insert_with(|| HashedStorage::new(false));
.or_default();
for (number, hash) in block_hashes {
let slot = U256::from(number % HISTORY_SERVE_WINDOW as u64);
history_storage.storage.insert(
Expand Down Expand Up @@ -1187,6 +1196,7 @@ where
fn ws_subscribe_logs(&self, mut filter: Filter, auth: AuthContext) -> BoxWsSubscriptionFut<'_> {
Box::pin(async move {
let provider = self.eth.api.provider().clone();
let api = self.eth.api.clone();
let caller = auth.caller;

let zone_tokens = self.zone_tokens();
Expand All @@ -1195,18 +1205,36 @@ where

let stream = provider
.canonical_state_stream()
.flat_map(|canon_state| futures::stream::iter(canon_state.block_receipts()))
.flat_map(move |(block_receipts, removed)| {
let all_logs = logs_utils::matching_block_logs_with_tx_hashes(
&filter,
block_receipts.block,
block_receipts.timestamp,
block_receipts
.tx_receipts
.iter()
.map(|(tx, receipt)| (*tx, receipt)),
removed,
);
.flat_map(move |canon_state| {
let reverted_chains = canon_state.reverted();
let committed_chain = canon_state.committed();
let reverted = reverted_chains.iter().flat_map(|chain| {
chain
.blocks_and_receipts()
.map(|(block, receipts)| (block, receipts, true))
});
let committed = committed_chain
.blocks_and_receipts()
.map(|(block, receipts)| (block, receipts, false));
let mut all_logs = Vec::new();

for (block, receipts, removed) in reverted.chain(committed) {
match logs_utils::matching_block_logs_with_tx_hashes(
api.converter(),
&filter,
block.sealed_header(),
block
.transactions_recovered()
.zip(receipts.iter())
.map(|(tx, receipt)| (*tx.tx_hash(), receipt)),
removed,
) {
Ok(logs) => all_logs.extend(logs),
Err(error) => {
tracing::error!(target: "rpc", %error, "Failed to convert logs");
}
}
}
futures::stream::iter(all_logs)
});

Expand Down Expand Up @@ -1484,12 +1512,11 @@ mod tests {
.with_database(revm::database::EmptyDB::default())
.build();
state.block_hashes.insert(number, hash);
let mut witness = ExecutionWitnessRecord::default();
let mut additional_state = HashedPostState::default();

record_block_hash_storage_proofs(&mut witness, &state);
record_block_hash_storage_proofs(&mut additional_state, &state);

let storage = witness
.hashed_state
let storage = additional_state
.storages
.get(&keccak256(HISTORY_STORAGE_ADDRESS))
.unwrap();
Expand Down
70 changes: 57 additions & 13 deletions crates/precompiles/src/account_keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
use alloy_primitives::Address;
use alloy_sol_types::SolInterface;
use tempo_contracts::precompiles::IAccountKeychain;
use tempo_precompiles::dispatch::abi_decoder_config_for_spec;

use crate::{
execution::{CallCheck, CallRules},
privacy::check_caller,
storage::StorageCtx,
};

/// Zone-specific rules applied before forwarding to upstream `AccountKeychain`.
Expand All @@ -15,7 +17,10 @@ pub(crate) struct AccountKeychainRules;

impl CallRules for AccountKeychainRules {
fn admit(&self, data: &[u8], caller: Address) -> CallCheck {
let Ok(call) = IAccountKeychain::IAccountKeychainCalls::abi_decode(data) else {
let Ok(call) = IAccountKeychain::IAccountKeychainCalls::abi_decode_with_config(
data,
abi_decoder_config_for_spec(StorageCtx::default().spec()),
) else {
// Preserve the upstream error and gas behavior for malformed or unknown calldata.
return CallCheck::Continue;
};
Expand Down Expand Up @@ -59,13 +64,26 @@ mod tests {
use super::*;
use alloy_primitives::{Address, B256};
use alloy_sol_types::{SolCall, SolError};
use tempo_chainspec::hardfork::TempoHardfork;
use tempo_zone_contracts::Unauthorized;

use crate::{
storage::StorageCtx,
test_utils::{test_context, test_storage_provider},
};

fn admit_at(
rules: &AccountKeychainRules,
data: &[u8],
caller: Address,
spec: TempoHardfork,
) -> CallCheck {
let mut ctx = test_context();
ctx.cfg.spec = spec;
let mut storage = test_storage_provider(&mut ctx, u64::MAX, true);
StorageCtx::enter(&mut storage, || rules.admit(data, caller))
}

fn assert_account_scoped<C: SolCall + Clone>(
rules: &AccountKeychainRules,
call: C,
Expand Down Expand Up @@ -167,21 +185,47 @@ mod tests {
let caller = Address::repeat_byte(0x11);
let rules = AccountKeychainRules;

let mut ctx = test_context();
let mut storage = test_storage_provider(&mut ctx, u64::MAX, true);
StorageCtx::enter(&mut storage, || {
assert!(matches!(
rules.admit(
&IAccountKeychain::getTransactionKeyCall {}.abi_encode(),
caller,
),
CallCheck::Continue
));
assert!(matches!(
rules.admit(
&IAccountKeychain::revokeKeyCall {
keyId: Address::repeat_byte(0x22),
}
.abi_encode(),
caller,
),
CallCheck::Continue
));
});
}

#[test]
fn t11_defers_noncanonical_address_calldata_to_upstream() {
let owner = Address::repeat_byte(0x11);
let outsider = Address::repeat_byte(0x22);
let rules = AccountKeychainRules;
let mut data = IAccountKeychain::getKeyCall {
account: owner,
keyId: Address::repeat_byte(0x33),
}
.abi_encode();
data[4] = 1;

assert!(matches!(
rules.admit(
&IAccountKeychain::getTransactionKeyCall {}.abi_encode(),
caller
),
CallCheck::Continue
admit_at(&rules, &data, outsider, TempoHardfork::T8),
CallCheck::Revert(data) if data == Unauthorized {}.abi_encode()
));
assert!(matches!(
rules.admit(
&IAccountKeychain::revokeKeyCall {
keyId: Address::repeat_byte(0x22),
}
.abi_encode(),
caller
),
admit_at(&rules, &data, outsider, TempoHardfork::T11),
CallCheck::Continue
));
}
Expand Down
44 changes: 42 additions & 2 deletions crates/precompiles/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ pub(crate) trait CallRules: 'static {
None
}

/// Applies pure Zone-specific admission rules before storage setup.
/// Applies Zone-specific admission rules.
fn admit(&self, _data: &[u8], _caller: Address) -> CallCheck {
CallCheck::Continue
}
Expand All @@ -115,7 +115,13 @@ pub(crate) fn create_precompile(
}

let (data, caller) = (input.data, input.caller);
if input.gas < input_cost(data.len()) {
let Ok(input_gas) = input_cost(env.cfg.spec, data.len()) else {
return Ok(PrecompileOutput::halt(
PrecompileHalt::OutOfGas,
input.reservoir,
));
};
if input.gas < input_gas {
return Ok(PrecompileOutput::halt(
PrecompileHalt::OutOfGas,
input.reservoir,
Expand Down Expand Up @@ -418,6 +424,40 @@ mod tests {
assert_eq!(rejected.bytes, Bytes::from_static(b"denied"));
}

#[test]
fn input_gas_threshold_tracks_t11() {
let calldata = [0u8; 32];

for (spec, required_gas) in [(TempoHardfork::T10, 6), (TempoHardfork::T11, 30)] {
let mut cfg = revm::context::CfgEnv::<TempoHardfork>::default();
cfg.spec = spec;
let env = ZonePrecompileEnv::new(
&cfg,
zone_hardfork::ZoneHardfork::Z0,
StorageActions::disabled(),
Rc::new(RefCell::new(NonCreditableSlots::empty())),
);
let precompile = create_precompile("InputGasTest", &env, NoCallRules, |_, _| {
Ok(StorageCtx::default().success_output(Bytes::new()))
});
let mut ctx = test_context();

let insufficient = precompile
.call(input(&mut ctx, &calldata, Address::ZERO, required_gas - 1))
.unwrap();
assert_eq!(
insufficient.halt_reason(),
Some(&PrecompileHalt::OutOfGas),
"{spec:?} must require {required_gas} input gas"
);

let sufficient = precompile
.call(input(&mut ctx, &calldata, Address::ZERO, required_gas))
.unwrap();
assert!(!sufficient.is_halt(), "{spec:?} must accept its exact cost");
}
}

struct FatalRules;

impl CallRules for FatalRules {
Expand Down
36 changes: 35 additions & 1 deletion crates/precompiles/src/nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
use alloy_primitives::Address;
use alloy_sol_types::SolInterface;
use tempo_contracts::precompiles::INonce;
use tempo_precompiles::dispatch::abi_decoder_config_for_spec;

use crate::{
execution::{CallCheck, CallRules},
privacy::check_caller,
storage::StorageCtx,
};

/// Zone-specific rules applied before forwarding to upstream `NonceManager`.
Expand All @@ -15,7 +17,10 @@ pub(crate) struct NonceRules;

impl CallRules for NonceRules {
fn admit(&self, data: &[u8], caller: Address) -> CallCheck {
let Ok(call) = INonce::INonceCalls::abi_decode(data) else {
let Ok(call) = INonce::INonceCalls::abi_decode_with_config(
data,
abi_decoder_config_for_spec(StorageCtx::default().spec()),
) else {
// Preserve the upstream error and gas behavior for malformed or unknown calldata.
return CallCheck::Continue;
};
Expand All @@ -32,13 +37,21 @@ mod tests {
use super::*;
use alloy_primitives::{Address, U256};
use alloy_sol_types::{SolCall, SolError};
use tempo_chainspec::hardfork::TempoHardfork;
use tempo_zone_contracts::Unauthorized;

use crate::{
storage::StorageCtx,
test_utils::{test_context, test_storage_provider},
};

fn admit_at(data: &[u8], caller: Address, spec: TempoHardfork) -> CallCheck {
let mut ctx = test_context();
ctx.cfg.spec = spec;
let mut storage = test_storage_provider(&mut ctx, u64::MAX, true);
StorageCtx::enter(&mut storage, || NonceRules.admit(data, caller))
}

#[test]
fn nonce_reads_allow_only_owner() {
let owner = Address::repeat_byte(0x11);
Expand Down Expand Up @@ -66,4 +79,25 @@ mod tests {
}
});
}

#[test]
fn t11_defers_noncanonical_address_calldata_to_upstream() {
let owner = Address::repeat_byte(0x11);
let outsider = Address::repeat_byte(0x22);
let mut data = INonce::getNonceCall {
account: owner,
nonceKey: U256::from(1),
}
.abi_encode();
data[4] = 1;

assert!(matches!(
admit_at(&data, outsider, TempoHardfork::T8),
CallCheck::Revert(data) if data == Unauthorized {}.abi_encode()
));
assert!(matches!(
admit_at(&data, outsider, TempoHardfork::T11),
CallCheck::Continue
));
}
}
Loading
Loading