From 6151a947e5c83a3e209515f35d165e4d9fdc6229 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Fri, 13 Feb 2026 14:41:29 -0500 Subject: [PATCH 1/8] poc of gas left as rng salt --- crates/context/interface/src/local.rs | 12 ++ crates/seismic/src/chain/rng_container.rs | 5 +- crates/seismic/src/chain/seismic_chain.rs | 105 ++++++++++++++++++ crates/seismic/src/evm.rs | 13 ++- .../src/precompiles/rng/domain_sep_rng.rs | 9 ++ .../seismic/src/precompiles/rng/precompile.rs | 3 +- 6 files changed, 143 insertions(+), 4 deletions(-) diff --git a/crates/context/interface/src/local.rs b/crates/context/interface/src/local.rs index 47e15c06..3544ed92 100644 --- a/crates/context/interface/src/local.rs +++ b/crates/context/interface/src/local.rs @@ -74,6 +74,18 @@ impl FrameStack { *index += 1; } + /// Returns a slice of all active (initialized) frames on the stack. + /// + /// Frames `0..=index` are always initialized, as the stack only grows + /// via `end_init`/`push` which set the index after initialization. + #[inline] + pub fn active_frames(&self) -> &[T] { + match self.index { + Some(idx) => &self.stack[..=idx], + None => &[], + } + } + /// Clears the stack by setting the index to 0. /// It does not destroy the stack. #[inline] diff --git a/crates/seismic/src/chain/rng_container.rs b/crates/seismic/src/chain/rng_container.rs index 66dd9c6c..b17b62ad 100644 --- a/crates/seismic/src/chain/rng_container.rs +++ b/crates/seismic/src/chain/rng_container.rs @@ -81,7 +81,7 @@ impl RngContainer { kernel_mode: RngMode, tx_hash: &B256, ) -> Result { - self.process_rng_with_key(pers, requested_output_len, kernel_mode, tx_hash, None) + self.process_rng_with_key(pers, requested_output_len, kernel_mode, tx_hash, None, 0 /*todo(dalton) temporary*/ ) } pub fn process_rng_with_key( @@ -91,6 +91,7 @@ impl RngContainer { kernel_mode: RngMode, tx_hash: &B256, live_key: Option, + total_gas_remaining: u64 ) -> Result { // Use live key for Execute mode, otherwise use default container if let Some(key) = live_key { @@ -98,7 +99,7 @@ impl RngContainer { // Note: live_key is only provided for RngMode::Execution let live_rng = RootRng::new(key); live_rng.append_tx(tx_hash); - + live_rng.append_gas_left(total_gas_remaining); let mut leaf_rng = live_rng.fork(pers); let mut rng_bytes = vec![0u8; requested_output_len]; leaf_rng.fill_bytes(&mut rng_bytes); diff --git a/crates/seismic/src/chain/seismic_chain.rs b/crates/seismic/src/chain/seismic_chain.rs index e734463c..1e4f7c87 100644 --- a/crates/seismic/src/chain/seismic_chain.rs +++ b/crates/seismic/src/chain/seismic_chain.rs @@ -11,6 +11,8 @@ use super::rng_container::RngContainer; pub struct SeismicChain { rng_container: RngContainer, live_rng_key: Option, + /// Total remaining gas across all active call frames, set before precompile dispatch. + gas_remaining_all_frames: u64, } impl SeismicChain { @@ -18,6 +20,7 @@ impl SeismicChain { Self { rng_container: RngContainer::new(root_vrf_key.clone()), live_rng_key: Some(root_vrf_key), + gas_remaining_all_frames: 0, } } @@ -25,6 +28,7 @@ impl SeismicChain { Self { rng_container: RngContainer::default(), live_rng_key, + gas_remaining_all_frames: 0, } } @@ -40,6 +44,14 @@ impl SeismicChain { &mut self.rng_container } + pub fn gas_remaining_all_frames(&self) -> u64 { + self.gas_remaining_all_frames + } + + pub fn set_gas_remaining_all_frames(&mut self, gas: u64) { + self.gas_remaining_all_frames = gas; + } + pub fn reset_rng(&mut self) { self.rng_container.reset_rng(); } @@ -59,6 +71,7 @@ impl SeismicChain { requested_output_len: usize, kernel_mode: RngMode, tx_hash: &B256, + total_gas_remaining: u64 ) -> Result { // Check if we should use live key for Execute mode let rng_key = match (&kernel_mode, &self.live_rng_key) { @@ -72,6 +85,98 @@ impl SeismicChain { kernel_mode, tx_hash, rng_key, + total_gas_remaining ) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use super::*; + use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; + + #[test] + fn test_execution_mode_same_inputs_same_output() { + let keypair = get_unsecure_sample_schnorrkel_keypair(); + let mut chain = SeismicChain::new(keypair); + + let tx_hash = B256::from([1u8; 32]); + let pers = b"test_pers"; + + let output1 = chain + .process_rng(pers, 32, RngMode::Execution, &tx_hash,1000) + .unwrap(); + let output2 = chain + .process_rng(pers, 32, RngMode::Execution, &tx_hash,1000) + .unwrap(); + + assert_eq!( + output1, output2, + "execution mode should produce identical output for same tx_hash and pers" + ); + } + + #[test] + fn test_execution_mode_different_pers_different_output() { + let keypair = get_unsecure_sample_schnorrkel_keypair(); + let mut chain = SeismicChain::new(keypair); + + let tx_hash = B256::from([1u8; 32]); + + let output1 = chain + .process_rng(b"pers_a", 32, RngMode::Execution, &tx_hash,1000) + .unwrap(); + let output2 = chain + .process_rng(b"pers_b", 32, RngMode::Execution, &tx_hash,1000) + .unwrap(); + + assert_ne!( + output1, output2, + "execution mode should produce different output for different pers" + ); + } + + #[test] + fn test_execution_mode_different_tx_hash_different_output() { + let keypair = get_unsecure_sample_schnorrkel_keypair(); + let mut chain = SeismicChain::new(keypair); + + let pers = b"test_pers"; + + let output1 = chain + .process_rng(pers, 32, RngMode::Execution, &B256::from([1u8; 32]),1000) + .unwrap(); + let output2 = chain + .process_rng(pers, 32, RngMode::Execution, &B256::from([2u8; 32]), 1000) + .unwrap(); + + assert_ne!( + output1, output2, + "execution mode should produce different output for different tx_hash" + ); + } + + #[test] + fn test_execution_mode_deterministic_across_chains() { + let keypair = get_unsecure_sample_schnorrkel_keypair(); + let mut chain1 = SeismicChain::new(keypair.clone()); + let mut chain2 = SeismicChain::new(keypair); + + let tx_hash = B256::from([1u8; 32]); + let pers = b"test_pers"; + + let output1 = chain1 + .process_rng(pers, 32, RngMode::Execution, &tx_hash, 1000) + .unwrap(); + let output2 = chain2 + .process_rng(pers, 32, RngMode::Execution, &tx_hash, 1000) + .unwrap(); + + assert_eq!( + output1, output2, + "execution mode should be deterministic across separate chains with same key" + ); + } +} diff --git a/crates/seismic/src/evm.rs b/crates/seismic/src/evm.rs index ba3a0ef9..e55ec286 100644 --- a/crates/seismic/src/evm.rs +++ b/crates/seismic/src/evm.rs @@ -116,7 +116,7 @@ where impl EvmTr for SeismicEvm where - CTX: ContextTr, + CTX: SeismicContextTr, I: InstructionProvider, P: PrecompileProvider, { @@ -152,6 +152,17 @@ where ItemOrResult<&mut Self::Frame, ::FrameResult>, ContextError<<::Db as Database>::Error>, > { + // Sum remaining gas across all active frames and stash on SeismicChain + // so precompiles can read the total gas available across the call stack. + let total_gas: u64 = self + .0 + .frame_stack + .active_frames() + .iter() + .map(|frame| frame.interpreter.gas.remaining()) + .sum(); + self.0.ctx.chain_mut().set_gas_remaining_all_frames(total_gas); + self.0.frame_init(frame_input) } diff --git a/crates/seismic/src/precompiles/rng/domain_sep_rng.rs b/crates/seismic/src/precompiles/rng/domain_sep_rng.rs index 68b6797b..ef22b84f 100644 --- a/crates/seismic/src/precompiles/rng/domain_sep_rng.rs +++ b/crates/seismic/src/precompiles/rng/domain_sep_rng.rs @@ -112,6 +112,15 @@ impl RootRng { inner.transcript.append_message(b"local-rng", &bytes); } + /// append bytes + /// + /// Append bytes to the RNG transcript + pub fn append_gas_left(&self, gas_left: u64) { + let mut inner = self.inner.borrow_mut(); + + inner.transcript.append_message(b"gas", &gas_left.to_le_bytes()) + } + /// Append an observed transaction hash to RNG transcript. pub fn append_tx(&self, tx_hash: &B256) { let mut inner = self.inner.borrow_mut(); diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index 6ed71511..c35c5dd0 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -126,6 +126,7 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - return Err(PrecompileError::OutOfGas); // Changed REVM_ERROR to PrecompileError } + let total_gas_remaining = evmctx.chain().gas_remaining_all_frames() + gas_limit; // Obtain kernel mode and transaction hash. let kernel_mode = evmctx.tx().rng_mode(); let tx_hash = evmctx.tx().tx_hash(); @@ -133,7 +134,7 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - // Let the container update its state and produce the random bytes. let output = evmctx .chain_mut() - .process_rng(&pers, requested_output_len, kernel_mode, &tx_hash) + .process_rng(&pers, requested_output_len, kernel_mode, &tx_hash,total_gas_remaining) .map_err(|e| PrecompileError::Other(e.to_string()))?; // Changed PCError to PrecompileError Ok(PrecompileOutput::new(gas_used, output)) From 604a5251d72692a0851dab809929be18ae3fac80 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Mon, 16 Feb 2026 14:15:12 -0500 Subject: [PATCH 2/8] simplify rng precompile to be statless/use enclave generated key --- Cargo.lock | 2 - crates/seismic/Cargo.toml | 2 - crates/seismic/src/chain/rng_container.rs | 163 +++-------- crates/seismic/src/chain/seismic_chain.rs | 102 +++---- crates/seismic/src/evm.rs | 69 ++--- crates/seismic/src/handler.rs | 22 +- crates/seismic/src/precompiles.rs | 28 +- .../src/precompiles/rng/domain_sep_rng.rs | 265 ++++++----------- .../seismic/src/precompiles/rng/precompile.rs | 169 ++++------- crates/seismic/src/precompiles/rng/test.rs | 276 +++++------------- crates/seismic/src/transaction/abstraction.rs | 25 -- 11 files changed, 349 insertions(+), 774 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9c6a217..d536fe35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4803,9 +4803,7 @@ dependencies = [ "auto_impl", "hkdf", "indicatif", - "merlin", "once_cell", - "rand_core 0.6.4", "revm", "rstest", "schnorrkel", diff --git a/crates/seismic/Cargo.toml b/crates/seismic/Cargo.toml index d3ba8c93..3efa7116 100644 --- a/crates/seismic/Cargo.toml +++ b/crates/seismic/Cargo.toml @@ -34,8 +34,6 @@ serde = { workspace = true, features = ["derive", "rc"], optional = true } # seismic schnorrkel = { version = "0.11.2", default-features = false } -merlin = { version = "3.0.0", default-features = false } -rand_core = { version = "0.6.4", default-features = false } hkdf = { version = "0.12", default-features = false } seismic-enclave = { workspace = true} sha2 = { workspace = true } diff --git a/crates/seismic/src/chain/rng_container.rs b/crates/seismic/src/chain/rng_container.rs index b17b62ad..0bac7750 100644 --- a/crates/seismic/src/chain/rng_container.rs +++ b/crates/seismic/src/chain/rng_container.rs @@ -1,137 +1,40 @@ -use core::fmt; -use rand_core::RngCore; +use crate::precompiles::rng::{domain_sep_rng::RootRng, precompile::calculate_gas_cost}; use revm::{ precompile::PrecompileError, primitives::{Bytes, B256}, }; - -use crate::transaction::abstraction::RngMode; -use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; - -use crate::precompiles::rng::{ - domain_sep_rng::{LeafRng, RootRng}, - precompile::{calculate_fill_cost, calculate_init_cost}, -}; - -pub struct RngContainer { - rng: RootRng, - leaf_rng: Option, -} - -impl Clone for RngContainer { - fn clone(&self) -> Self { - Self { - rng: self.rng.clone(), - leaf_rng: None, - } - } -} - -impl fmt::Debug for RngContainer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Hide internal details of the RNG container. - write!(f, "Kernel {{ }}") - } -} - -impl Default for RngContainer { - fn default() -> Self { - Self { - rng: RootRng::new(get_unsecure_sample_schnorrkel_keypair()), - leaf_rng: None, - } - } +use schnorrkel::ExpansionMode; + +/// Derives random bytes for the RNG precompile. +/// +/// Each call is fully stateless: a fresh `RootRng` is constructed from the +/// provided key, domain separation data (tx_hash, gas_left) is appended, +/// and bytes are derived via HKDF-SHA256. +/// +/// - **Execution mode** (`live_key = Some(key)`): uses the enclave-provided key. +/// Deterministic for the same (key, tx_hash, gas_left, pers). +/// - **Simulation mode** (`live_key = None`): generates a random key via `OsRng`. +/// Non-deterministic by design (each call gets a fresh random key). +pub fn derive_rng_output( + pers: &[u8], + requested_output_len: usize, + tx_hash: &B256, + live_key: Option, + total_gas_remaining: u64, +) -> Result { + let key = live_key.unwrap_or_else(|| { + schnorrkel::MiniSecretKey::generate() + .expand(ExpansionMode::Uniform) + .into() + }); + + let mut rng = RootRng::new(key); + rng.append_tx(tx_hash); + rng.append_gas_left(total_gas_remaining); + let rng_bytes = rng.derive_bytes(pers, requested_output_len); + Ok(Bytes::from(rng_bytes)) } -impl RngContainer { - pub fn new(root_vrf_key: schnorrkel::Keypair) -> Self { - Self { - rng: RootRng::new(root_vrf_key), - leaf_rng: None, - } - } -} - -impl RngContainer { - pub fn reset_rng(&mut self) { - let root_vrf_key = self.rng.get_root_vrf_key(); - self.rng = RootRng::new(root_vrf_key); - self.leaf_rng = None; - } - - /// Appends entropy to the root RNG if in Simulation mode. - pub fn maybe_append_entropy(&mut self, mode: RngMode) { - if mode == RngMode::Simulation { - self.rng.append_local_entropy(); - } - } - - pub fn calculate_gas_cost(&self, pers: &[u8], requested_output_len: usize) -> u64 { - match self.leaf_rng.as_ref() { - Some(_) => calculate_fill_cost(requested_output_len), - None => calculate_init_cost(pers.len()) - .saturating_add(calculate_fill_cost(requested_output_len)), - } - } - - pub fn process_rng( - &mut self, - pers: &[u8], - requested_output_len: usize, - kernel_mode: RngMode, - tx_hash: &B256, - ) -> Result { - self.process_rng_with_key(pers, requested_output_len, kernel_mode, tx_hash, None, 0 /*todo(dalton) temporary*/ ) - } - - pub fn process_rng_with_key( - &mut self, - pers: &[u8], - requested_output_len: usize, - kernel_mode: RngMode, - tx_hash: &B256, - live_key: Option, - total_gas_remaining: u64 - ) -> Result { - // Use live key for Execute mode, otherwise use default container - if let Some(key) = live_key { - // Create a temporary RNG with the live key for this operation - // Note: live_key is only provided for RngMode::Execution - let live_rng = RootRng::new(key); - live_rng.append_tx(tx_hash); - live_rng.append_gas_left(total_gas_remaining); - let mut leaf_rng = live_rng.fork(pers); - let mut rng_bytes = vec![0u8; requested_output_len]; - leaf_rng.fill_bytes(&mut rng_bytes); - Ok(Bytes::from(rng_bytes)) - } else { - // Use the default container's RNG - self.maybe_append_entropy(kernel_mode); - self.rng.append_tx(tx_hash); - - // Initialize the leaf RNG if not done already. - if self.leaf_rng.is_none() { - let leaf_rng = self.rng.fork(pers); - self.leaf_rng = Some(leaf_rng); - } - - // Get the random bytes. - // SAFETY: leaf_rng is guaranteed to be Some - initialized in the if block above - #[allow(clippy::unwrap_used)] - let leaf_rng = self.leaf_rng.as_mut().unwrap(); - let mut rng_bytes = vec![0u8; requested_output_len]; - leaf_rng.fill_bytes(&mut rng_bytes); - Ok(Bytes::from(rng_bytes)) - } - } - - #[cfg(test)] - pub fn root_rng(&self) -> &RootRng { - &self.rng - } - - #[cfg(test)] - pub fn leaf_rng(&self) -> &Option { - &self.leaf_rng - } +pub fn rng_gas_cost(requested_output_len: usize) -> u64 { + calculate_gas_cost(requested_output_len) } diff --git a/crates/seismic/src/chain/seismic_chain.rs b/crates/seismic/src/chain/seismic_chain.rs index 1e4f7c87..5d3b9e30 100644 --- a/crates/seismic/src/chain/seismic_chain.rs +++ b/crates/seismic/src/chain/seismic_chain.rs @@ -3,13 +3,10 @@ use revm::{ primitives::{Bytes, B256}, }; -use crate::transaction::abstraction::RngMode; - -use super::rng_container::RngContainer; +use super::rng_container::{derive_rng_output, rng_gas_cost}; #[derive(Clone, Debug, Default)] pub struct SeismicChain { - rng_container: RngContainer, live_rng_key: Option, /// Total remaining gas across all active call frames, set before precompile dispatch. gas_remaining_all_frames: u64, @@ -18,7 +15,6 @@ pub struct SeismicChain { impl SeismicChain { pub fn new(root_vrf_key: schnorrkel::Keypair) -> Self { Self { - rng_container: RngContainer::new(root_vrf_key.clone()), live_rng_key: Some(root_vrf_key), gas_remaining_all_frames: 0, } @@ -26,22 +22,13 @@ impl SeismicChain { pub fn with_live_rng_key(live_rng_key: Option) -> Self { Self { - rng_container: RngContainer::default(), live_rng_key, gas_remaining_all_frames: 0, } } pub fn set_rng_key(&mut self, root_vrf_key: schnorrkel::Keypair) { - self.rng_container = RngContainer::new(root_vrf_key); - } - - pub fn rng_container(&self) -> &RngContainer { - &self.rng_container - } - - pub fn rng_container_mut(&mut self) -> &mut RngContainer { - &mut self.rng_container + self.live_rng_key = Some(root_vrf_key); } pub fn gas_remaining_all_frames(&self) -> u64 { @@ -52,40 +39,23 @@ impl SeismicChain { self.gas_remaining_all_frames = gas; } - pub fn reset_rng(&mut self) { - self.rng_container.reset_rng(); - } - - pub fn maybe_append_entropy(&mut self, mode: RngMode) { - self.rng_container.maybe_append_entropy(mode); - } - - pub fn calculate_gas_cost(&self, pers: &[u8], requested_output_len: usize) -> u64 { - self.rng_container - .calculate_gas_cost(pers, requested_output_len) + pub fn calculate_gas_cost(&self, requested_output_len: usize) -> u64 { + rng_gas_cost(requested_output_len) } pub fn process_rng( - &mut self, + &self, pers: &[u8], requested_output_len: usize, - kernel_mode: RngMode, tx_hash: &B256, - total_gas_remaining: u64 + total_gas_remaining: u64, ) -> Result { - // Check if we should use live key for Execute mode - let rng_key = match (&kernel_mode, &self.live_rng_key) { - (RngMode::Execution, Some(live_key)) => Some(live_key.clone()), - _ => None, - }; - - self.rng_container.process_rng_with_key( + derive_rng_output( pers, requested_output_len, - kernel_mode, tx_hash, - rng_key, - total_gas_remaining + self.live_rng_key.clone(), + total_gas_remaining, ) } } @@ -100,17 +70,13 @@ mod tests { #[test] fn test_execution_mode_same_inputs_same_output() { let keypair = get_unsecure_sample_schnorrkel_keypair(); - let mut chain = SeismicChain::new(keypair); + let chain = SeismicChain::new(keypair); let tx_hash = B256::from([1u8; 32]); let pers = b"test_pers"; - let output1 = chain - .process_rng(pers, 32, RngMode::Execution, &tx_hash,1000) - .unwrap(); - let output2 = chain - .process_rng(pers, 32, RngMode::Execution, &tx_hash,1000) - .unwrap(); + let output1 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + let output2 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); assert_eq!( output1, output2, @@ -121,16 +87,12 @@ mod tests { #[test] fn test_execution_mode_different_pers_different_output() { let keypair = get_unsecure_sample_schnorrkel_keypair(); - let mut chain = SeismicChain::new(keypair); + let chain = SeismicChain::new(keypair); let tx_hash = B256::from([1u8; 32]); - let output1 = chain - .process_rng(b"pers_a", 32, RngMode::Execution, &tx_hash,1000) - .unwrap(); - let output2 = chain - .process_rng(b"pers_b", 32, RngMode::Execution, &tx_hash,1000) - .unwrap(); + let output1 = chain.process_rng(b"pers_a", 32, &tx_hash, 1000).unwrap(); + let output2 = chain.process_rng(b"pers_b", 32, &tx_hash, 1000).unwrap(); assert_ne!( output1, output2, @@ -141,15 +103,15 @@ mod tests { #[test] fn test_execution_mode_different_tx_hash_different_output() { let keypair = get_unsecure_sample_schnorrkel_keypair(); - let mut chain = SeismicChain::new(keypair); + let chain = SeismicChain::new(keypair); let pers = b"test_pers"; let output1 = chain - .process_rng(pers, 32, RngMode::Execution, &B256::from([1u8; 32]),1000) + .process_rng(pers, 32, &B256::from([1u8; 32]), 1000) .unwrap(); let output2 = chain - .process_rng(pers, 32, RngMode::Execution, &B256::from([2u8; 32]), 1000) + .process_rng(pers, 32, &B256::from([2u8; 32]), 1000) .unwrap(); assert_ne!( @@ -161,22 +123,34 @@ mod tests { #[test] fn test_execution_mode_deterministic_across_chains() { let keypair = get_unsecure_sample_schnorrkel_keypair(); - let mut chain1 = SeismicChain::new(keypair.clone()); - let mut chain2 = SeismicChain::new(keypair); + let chain1 = SeismicChain::new(keypair.clone()); + let chain2 = SeismicChain::new(keypair); let tx_hash = B256::from([1u8; 32]); let pers = b"test_pers"; - let output1 = chain1 - .process_rng(pers, 32, RngMode::Execution, &tx_hash, 1000) - .unwrap(); - let output2 = chain2 - .process_rng(pers, 32, RngMode::Execution, &tx_hash, 1000) - .unwrap(); + let output1 = chain1.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + let output2 = chain2.process_rng(pers, 32, &tx_hash, 1000).unwrap(); assert_eq!( output1, output2, "execution mode should be deterministic across separate chains with same key" ); } + + #[test] + fn test_simulation_mode_non_deterministic() { + // No live key = simulation mode (random key per call) + let chain = SeismicChain::default(); + let tx_hash = B256::from([1u8; 32]); + let pers = b"test_pers"; + + let output1 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + let output2 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + + assert_ne!( + output1, output2, + "simulation mode should produce different output each call" + ); + } } diff --git a/crates/seismic/src/evm.rs b/crates/seismic/src/evm.rs index e55ec286..f0f519c9 100644 --- a/crates/seismic/src/evm.rs +++ b/crates/seismic/src/evm.rs @@ -161,7 +161,10 @@ where .iter() .map(|frame| frame.interpreter.gas.remaining()) .sum(); - self.0.ctx.chain_mut().set_gas_remaining_all_frames(total_gas); + self.0 + .ctx + .chain_mut() + .set_gas_remaining_all_frames(total_gas); self.0.frame_init(frame_input) } @@ -202,24 +205,21 @@ mod tests { use super::*; use crate::precompiles::rng; - use crate::precompiles::rng::domain_sep_rng::RootRng; - use crate::precompiles::rng::precompile::{calculate_fill_cost, calculate_init_cost}; + use crate::precompiles::rng::precompile::calculate_gas_cost; use crate::transaction::abstraction::SeismicTransaction; use crate::{ DefaultSeismicContext, SeismicBuilder, SeismicChain, SeismicContext, SeismicHaltReason, SeismicSpecId, }; use anyhow::bail; - use rand_core::RngCore; use revm::context::result::{ExecutionResult, Output, ResultAndState}; use revm::context::{BlockEnv, CfgEnv, Context, ContextTr, JournalTr, TxEnv}; use revm::database::{EmptyDB, InMemoryDB, BENCH_CALLER}; use revm::interpreter::gas::calculate_initial_tx_gas; use revm::interpreter::InitialAndFloorGas; use revm::precompile::u64_to_address; - use revm::primitives::{Address, Bytes, TxKind, B256, U256}; + use revm::primitives::{Address, Bytes, TxKind, U256}; use revm::{ExecuteCommitEvm, ExecuteEvm, Journal}; - use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; // === Fixture data === @@ -399,6 +399,7 @@ mod tests { spec: SeismicSpecId, bytes_requested: u32, personalization: Vec, + keypair: schnorrkel::Keypair, ) -> Context< BlockEnv, SeismicTransaction, @@ -414,11 +415,9 @@ mod tests { let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas(spec.into(), &input[..], false, 0, 0, 0); - let total_gas = initial_gas - + calculate_init_cost(personalization.len()) - + calculate_fill_cost(bytes_requested as usize); + let total_gas = initial_gas + calculate_gas_cost(bytes_requested as usize); - Context::seismic() + Context::seismic_with_rng_key(keypair) .modify_tx_chained(|tx| { tx.base.kind = TxKind::Call(u64_to_address(rng::precompile::RNG_ADDRESS)); tx.base.data = input; @@ -428,54 +427,46 @@ mod tests { } #[test] - fn test_rng_precompile_expected_output_and_cleared() { - // Variables + fn test_rng_precompile_expected_output() { + use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; + let bytes_requested: u32 = 32; let personalization = vec![0xAA, 0xBB, 0xCC, 0xDD]; + let keypair = get_unsecure_sample_schnorrkel_keypair(); // Get EVM output let ctx = rng_test_tx( SeismicSpecId::MERCURY, bytes_requested, personalization.clone(), + keypair.clone(), ); let mut evm = ctx.build_seismic_evm(); let output = evm.replay().unwrap(); - let evm_output = output.result.into_output().unwrap(); - // reconstruct expected output - let root_rng = RootRng::test_default(); - root_rng.append_tx(&B256::default()); - let mut leaf_rng = root_rng.fork(&personalization); - let mut rng_bytes = vec![0u8; bytes_requested as usize]; - leaf_rng.fill_bytes(&mut rng_bytes); - assert_eq!( - Bytes::from(rng_bytes), + // Verify output is 32 bytes and non-zero + assert_eq!(evm_output.len(), 32, "RNG output should be 32 bytes"); + assert_ne!( evm_output, - "expected output and evm output should be equal" + Bytes::from(vec![0u8; 32]), + "RNG output should not be all zeros" ); - // check root rng state is reset post execution - let expected_root_rng_state = ( - get_unsecure_sample_schnorrkel_keypair().public.to_bytes(), - true, - true, - 0 as u64, - ); - assert!( - evm.ctx().chain().rng_container().leaf_rng().is_none(), - "leaf rng should be none post execution" + // Verify determinism: same inputs produce same output + let ctx2 = rng_test_tx( + SeismicSpecId::MERCURY, + bytes_requested, + personalization, + keypair, ); + let mut evm2 = ctx2.build_seismic_evm(); + let output2 = evm2.replay().unwrap(); + let evm_output2 = output2.result.into_output().unwrap(); assert_eq!( - evm.ctx() - .chain() - .rng_container() - .root_rng() - .state_snapshot(), - expected_root_rng_state, - "root rng state should be as expected" + evm_output, evm_output2, + "same inputs should produce same output" ); } } diff --git a/crates/seismic/src/handler.rs b/crates/seismic/src/handler.rs index 111bb12f..adc4e44e 100644 --- a/crates/seismic/src/handler.rs +++ b/crates/seismic/src/handler.rs @@ -46,13 +46,14 @@ where /// Processes the final execution output. /// - /// This method, retrieves the final state from the journal, converts internal results to the external output format. - /// Internal state is cleared and EVM is prepared for the next transaction. + /// Retrieves the final state from the journal, converts internal results + /// to the external output format. Internal state is cleared and EVM is + /// prepared for the next transaction. /// - /// Seismic Addendum - /// Given that we can't yet pass instruction_result which aren't in the InstructionResult enum, - /// We leverage context_error to bubble up our instruction set specific errors! We also clear - /// the rng state on returns that won't go through catch_error. + /// Seismic Addendum: + /// Given that we can't yet pass instruction_result which aren't in the + /// InstructionResult enum, we leverage context_error to bubble up our + /// instruction set specific errors. #[inline] fn execution_result( &mut self, @@ -65,10 +66,8 @@ where if let Some(seismic_reason) = SeismicHaltReason::try_from_error_string(&e.to_string()) { - // Same as catch error, except don't discard tx evm.ctx().local_mut().clear(); evm.frame_stack().clear(); - evm.ctx().chain_mut().reset_rng(); return Ok(ExecutionResult::Halt { reason: seismic_reason, @@ -82,12 +81,9 @@ where let exec_result = post_execution::output(evm.ctx(), result); - // commit transaction evm.ctx().journal_mut().commit_tx(); evm.ctx().local_mut().clear(); evm.frame_stack().clear(); - // ...and we also reset the RNG - evm.ctx().chain_mut().reset_rng(); Ok(exec_result) } @@ -95,7 +91,6 @@ where /// Handles cleanup when an error occurs during execution. /// /// Ensures the journal state is properly cleared before propagating the error. - /// Also ensures the rng has been reset. /// On happy path journal is cleared in [`Handler::output`] method. #[inline] fn catch_error( @@ -103,12 +98,9 @@ where evm: &mut Self::Evm, error: Self::Error, ) -> Result, Self::Error> { - // Same as in normal ETH handler... evm.ctx().local_mut().clear(); evm.ctx().journal_mut().discard_tx(); evm.frame_stack().clear(); - // ...except we also reset the RNG - evm.ctx().chain_mut().reset_rng(); Err(error) } } diff --git a/crates/seismic/src/precompiles.rs b/crates/seismic/src/precompiles.rs index 89714a59..e718c44a 100644 --- a/crates/seismic/src/precompiles.rs +++ b/crates/seismic/src/precompiles.rs @@ -213,7 +213,7 @@ mod tests { use revm::{ database::EmptyDB, interpreter::{CallScheme, CallValue}, - primitives::{hex, U256}, + primitives::U256, }; use crate::{DefaultSeismicContext, SeismicContext}; @@ -260,9 +260,12 @@ mod tests { #[test] fn test_seismic_precompiles_rng() { + use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; + let mut precompiles = SeismicPrecompiles::>::new_with_spec(SeismicSpecId::MERCURY); - let mut context = SeismicContext::::seismic(); + let keypair = get_unsecure_sample_schnorrkel_keypair(); + let mut context = SeismicContext::::seismic_with_rng_key(keypair); let rng_address = *precompiles .stateful_precompiles .addresses() @@ -287,13 +290,6 @@ mod tests { let output_bytes = interpreter_result.output; assert_eq!(output_bytes.len(), 32, "RNG output should be 32 bytes"); - assert_eq!( - output_bytes, - Bytes::from(hex!( - "6205fa1fc78e42116f1b370e200a867805679032f64ab68256ae59d678dc441d" - )), - "RNG precompile should return successfully" - ); let gas_used = input.gas_limit - interpreter_result.gas.remaining(); assert!( @@ -302,6 +298,7 @@ mod tests { gas_used ); + // Second call with same inputs should produce same output (deterministic with live key) let result2 = precompiles.run(&mut context, &input); assert!(result2.is_ok(), "Second RNG call should succeed"); @@ -317,16 +314,15 @@ mod tests { ); let gas_used2 = input.gas_limit - interpreter_result2.gas.remaining(); - assert!( - gas_used2 < gas_used, - "Second call should use less gas, used {} vs first call {}", - gas_used2, - gas_used + assert_eq!( + gas_used2, gas_used, + "Both calls should cost the same, got {} vs {}", + gas_used2, gas_used ); - assert_ne!( + assert_eq!( output_bytes, output_bytes2, - "Subsequent RNG calls should return different outputs" + "Same inputs with live key should produce identical output (stateless)" ); } diff --git a/crates/seismic/src/precompiles/rng/domain_sep_rng.rs b/crates/seismic/src/precompiles/rng/domain_sep_rng.rs index ef22b84f..8e260c9e 100644 --- a/crates/seismic/src/precompiles/rng/domain_sep_rng.rs +++ b/crates/seismic/src/precompiles/rng/domain_sep_rng.rs @@ -1,201 +1,116 @@ -//! This module provides a domain separation RNG for the Seismic chain. -//! It uses the Merlin transcript to generate a root RNG that is used to derive -//! a leaf RNG for each transaction. -//! The Merlin transcript is initialized with a hash of the block environment. -//! The Merlin transcript is then forked for each transaction -//! The leaf RNG is then used to generate random bytes. +//! Domain-separated RNG using HKDF-SHA256 with a Schnorrkel key. //! -//! This module is heavily inspired Oasis Network's RNG implementation. -use merlin::{Transcript, TranscriptRng}; -use rand_core::{CryptoRng, OsRng, RngCore}; +//! For each precompile call, random bytes are derived via: +//! ```text +//! HKDF-SHA256( +//! ikm = schnorrkel_keypair.secret.to_bytes(), // 64-byte expanded secret key +//! salt = b"seismic rng context", +//! info = domain_data || b"pers" || pers +//! ) → output bytes +//! ``` +//! +//! Each call constructs a fresh `RootRng`, appends tx_hash + gas_left, +//! then derives output. There is no persistent state between calls. +use hkdf::Hkdf; use revm::primitives::B256; pub use schnorrkel::keys::Keypair as SchnorrkelKeypair; use seismic_enclave::get_unsecure_sample_schnorrkel_keypair; -use std::{cell::RefCell, rc::Rc}; +use sha2::Sha256; -/// RNG domain separation context. -const RNG_CONTEXT: &[u8] = b"seismic rng context"; +/// RNG domain separation salt. +const RNG_SALT: &[u8] = b"seismic rng context"; -/// A root RNG that can be used to derive domain-separated leaf RNGs. +/// A stateless RNG that derives output bytes via HKDF-SHA256. +/// +/// Constructed fresh for each precompile call. Domain separation data +/// (tx hash, gas left) is appended before derivation. pub struct RootRng { - inner: Rc>, -} - -struct Inner { - /// The VRF key for the block - root_vrf_key: SchnorrkelKeypair, - /// Merlin transcript for initializing the RNG. - transcript: Transcript, - /// A transcript-based RNG (when initialized). - rng: Option, - /// the transcript used to initialize the rng, saved for cloning - cloning_transcript: Option, - /// number of forks, saved for cloning - num_forks: u64, -} - -impl Clone for RootRng { - #[allow(clippy::unwrap_used)] // cloning_transcript is always set when rng is Some - fn clone(&self) -> Self { - let inner = self.inner.borrow_mut(); - let rng_copy: Option; - let root_vrf = inner.root_vrf_key.clone(); - if inner.rng.is_some() { - // make a new rng with the same transcript and vrf key - let cloning_transcript = inner.cloning_transcript.as_ref().unwrap().clone(); - - let mut rng = root_vrf - .vrf_create_hash(cloning_transcript) - .make_merlin_rng(&[]); - - // fast foward the rng to the same point as the original - // By assumption, fork() is the only place root TranscriptRng is used - for _ in 0..inner.num_forks { - let mut bytes = [0u8; 32]; - rng.fill_bytes(&mut bytes); - } - - rng_copy = Some(rng); - } else { - rng_copy = None; - } - - let new_inner = Inner { - root_vrf_key: root_vrf, - transcript: inner.transcript.clone(), - rng: rng_copy, - cloning_transcript: inner.cloning_transcript.clone(), - num_forks: inner.num_forks, - }; - - Self { - inner: Rc::new(RefCell::new(new_inner)), - } - } + /// The 64-byte expanded secret key from the schnorrkel keypair. + key_bytes: [u8; 64], + /// Accumulated domain separation info (tx hashes, gas values, etc.). + domain_data: Vec, } impl RootRng { - /// Create a new root RNG. - pub fn new(root_vrf_key: SchnorrkelKeypair) -> Self { + /// Create a new root RNG from a schnorrkel keypair. + pub fn new(keypair: SchnorrkelKeypair) -> Self { + let key_bytes = keypair.secret.to_bytes(); Self { - inner: Rc::new(RefCell::new(Inner { - root_vrf_key, - transcript: Transcript::new(RNG_CONTEXT), - rng: None, - cloning_transcript: None, - num_forks: 0, - })), + key_bytes, + domain_data: Vec::new(), } } - pub fn get_root_vrf_key(&self) -> SchnorrkelKeypair { - self.inner.borrow().root_vrf_key.clone() - } - - /// A default rng for testing that loads a sample keypair. - /// We do not implement the Default trait becuase + /// A default RNG for testing that loads a sample keypair. + /// We do not implement the Default trait because /// it might be misleading or error-prone. pub fn test_default() -> Self { Self::new(get_unsecure_sample_schnorrkel_keypair()) } - /// Append local entropy to the root RNG. - /// - /// # Non-determinism - /// - /// Using this method will result in the RNG being non-deterministic. - pub fn append_local_entropy(&self) { - let mut bytes = [0u8; 32]; - OsRng.fill_bytes(&mut bytes); - - let mut inner = self.inner.borrow_mut(); - inner.transcript.append_message(b"local-rng", &bytes); - } - - /// append bytes - /// - /// Append bytes to the RNG transcript - pub fn append_gas_left(&self, gas_left: u64) { - let mut inner = self.inner.borrow_mut(); - - inner.transcript.append_message(b"gas", &gas_left.to_le_bytes()) - } - - /// Append an observed transaction hash to RNG transcript. - pub fn append_tx(&self, tx_hash: &B256) { - let mut inner = self.inner.borrow_mut(); - inner.transcript.append_message(b"tx", tx_hash.as_ref()); + /// Append a transaction hash to the domain separation data. + pub fn append_tx(&mut self, tx_hash: &B256) { + self.domain_data.extend_from_slice(b"tx"); + self.domain_data.extend_from_slice(tx_hash.as_ref()); } - /// Append an observed subcontext to RNG transcript. - pub fn append_subcontext(&self) { - let mut inner = self.inner.borrow_mut(); - inner.transcript.append_message(b"subctx", &[]); + /// Append the remaining gas to the domain separation data. + pub fn append_gas_left(&mut self, gas_left: u64) { + self.domain_data.extend_from_slice(b"gas"); + self.domain_data.extend_from_slice(&gas_left.to_le_bytes()); } - /// Create an independent leaf RNG using this RNG as its parent. - pub fn fork(&self, pers: &[u8]) -> LeafRng { - let mut inner = self.inner.borrow_mut(); - - // Ensure the RNG is initialized and initialize it if not. - if inner.rng.is_none() { - // Initialize the root RNG. - inner.cloning_transcript = Some(inner.transcript.clone()); - let root_key = inner.root_vrf_key.clone(); - - let rng = root_key - .vrf_create_hash(&mut inner.transcript) - .make_merlin_rng(&[]); - - inner.rng = Some(rng); + /// Derive `len` random bytes using HKDF-SHA256 with the given personalization. + /// + /// The HKDF info parameter is: `domain_data || b"pers" || pers`. + /// This is a stateless operation — same inputs always produce the same output. + /// + /// For outputs larger than 255 * 32 = 8160 bytes (the HKDF-SHA256 limit), + /// this uses counter-mode chunking internally. + pub fn derive_bytes(&self, pers: &[u8], len: usize) -> Vec { + let hkdf = Hkdf::::new(Some(RNG_SALT), &self.key_bytes); + + // Build info: domain_data || b"pers" || pers + let mut info = Vec::with_capacity(self.domain_data.len() + 4 + pers.len()); + info.extend_from_slice(&self.domain_data); + info.extend_from_slice(b"pers"); + info.extend_from_slice(pers); + + // HKDF-Expand has a max output of 255 * HashLen (8160 bytes for SHA-256). + // For larger outputs, use counter-mode chunking. + const MAX_HKDF_OUTPUT: usize = 255 * 32; + + if len <= MAX_HKDF_OUTPUT { + let mut output = vec![0u8; len]; + // SAFETY: len <= MAX_HKDF_OUTPUT so expand cannot fail + #[allow(clippy::expect_used)] + hkdf.expand(&info, &mut output) + .expect("HKDF expand cannot fail for len <= 8160"); + output + } else { + // Counter-mode: chunk the output into MAX_HKDF_OUTPUT-sized pieces, + // each with a unique counter suffix in info. + let mut output = Vec::with_capacity(len); + let mut chunk_idx: u32 = 0; + while output.len() < len { + let remaining = len - output.len(); + let chunk_len = remaining.min(MAX_HKDF_OUTPUT); + let mut chunk = vec![0u8; chunk_len]; + + let mut chunk_info = Vec::with_capacity(info.len() + 4 /* counter */); + chunk_info.extend_from_slice(&info); + chunk_info.extend_from_slice(&chunk_idx.to_le_bytes()); + + // SAFETY: chunk_len <= MAX_HKDF_OUTPUT + #[allow(clippy::expect_used)] + hkdf.expand(&chunk_info, &mut chunk) + .expect("HKDF expand cannot fail for chunk_len <= 8160"); + + output.extend_from_slice(&chunk); + chunk_idx += 1; + } + output.truncate(len); + output } - - // Generate the leaf RNG. - inner.transcript.append_message(b"fork", pers); - - let rng_builder = inner.transcript.build_rng(); - // SAFETY: rng is initialized in the block above if it was None - #[allow(clippy::expect_used)] - let parent_rng = inner.rng.as_mut().expect("rng must be initialized"); - let rng = rng_builder.finalize(parent_rng); - - // Increment the number of forks - inner.num_forks += 1; - - LeafRng(rng) - } - - #[cfg(test)] - pub fn state_snapshot(&self) -> ([u8; 32], bool, bool, u64) { - let inner = self.inner.borrow_mut(); - ( - inner.root_vrf_key.clone().public.to_bytes(), - inner.rng.is_none(), - inner.cloning_transcript.is_none(), - inner.num_forks, - ) - } -} - -/// A leaf RNG. -pub struct LeafRng(TranscriptRng); - -impl RngCore for LeafRng { - fn next_u32(&mut self) -> u32 { - self.0.next_u32() - } - - fn next_u64(&mut self) -> u64 { - self.0.next_u64() - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest) - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { - self.0.try_fill_bytes(dest) } } - -impl CryptoRng for LeafRng {} diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index c35c5dd0..d4dec400 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -1,8 +1,5 @@ use revm::{ - context::ContextTr, - precompile::{ - calc_linear_cost_u32, u64_to_address, PrecompileError, PrecompileOutput, PrecompileResult, - }, + precompile::{u64_to_address, PrecompileError, PrecompileOutput, PrecompileResult}, primitives::Bytes, }; @@ -15,10 +12,10 @@ use crate::{ Constants & Setup -------------------------------------------------------------------------- */ -// The RNG precompile is a stateful precompile based on Merlin transcripts -// At each transaction in a block executes, the tx hash is appended to -// the transcript as domain seperation, causing identical transactions -// to produce different randomness +// The RNG precompile derives random bytes via HKDF-SHA256 using a schnorrkel key. +// Each call is stateless: the same (key, tx_hash, gas_left, pers) always produces +// the same output. Domain separation comes from tx_hash and gas_left appended +// to the HKDF info parameter. pub const RNG_ADDRESS: u64 = 100; // Hex address `0x64`. pub fn rng_precompile_iter( @@ -31,8 +28,17 @@ pub fn rng_precompile() -> StatefulPrecompileWithAddress< } const MIN_INPUT_LENGTH: usize = 2; -const RNG_INIT_BASE: u64 = 3500; -const STROBE128WORD: u64 = 5; + +/// Base cost for the HKDF-SHA256 derivation. This covers: +/// - HKDF-Extract: one HMAC-SHA256 (two SHA-256 passes over the 64-byte key) +/// - HKDF-Expand: one or more HMAC-SHA256 passes to produce output +/// - Conservative buffer for future adjustments +const RNG_BASE_COST: u64 = 3500; + +/// Per-word cost for output bytes. Each 32-byte word of output requires +/// an additional HMAC-SHA256 round in the HKDF-Expand phase. +/// Based on SHA-256 EVM pricing (~6 gas/word) adjusted for HKDF overhead. +const RNG_WORD_COST: u64 = 5; /* -------------------------------------------------------------------------- Precompile Logic @@ -48,70 +54,21 @@ Precompile Logic /// /// ## Overview /// We interpret the input as a `[u8]` slice of bytes used as personalization -/// for the RNG entropy. +/// for the RNG derivation. /// -/// Using the pers bytes, the block rng transcript, and the block VRF key, -/// we produce a leaf RNG that implements the `RngCore` interface and query -/// it for bytes. +/// Using HKDF-SHA256 with the schnorrkel keypair as input key material, +/// and domain separation data (tx_hash, gas_left) plus personalization as the +/// HKDF info parameter, we derive the requested number of random bytes. /// /// ## Gas Cost /// -/// ### Pricing Fundamental Operations -/// The RNG precompile uses Merlin transcripts that rely on the Strobe128 hash function. -/// Strobe uses the keccak256 sponge, which has an EVM opcode cost of -/// `g=30+6×ceil(input size/32)`. We have a more complex initialization than SHA, -/// so we price a base cost of 100 gas. However, Strobe128 is designed for 128-bit security -/// (instead of SHA3's 256-bit security), which allows it to work faster. The dominating cost -/// for the keccak256 sponges is the keccak256 permutation. For SHA3, you permute -/// once per 136 bytes of data absorbed. Ethereum simplifies this cost calculation as -/// 6 gas per 32-byte word absorbed. Strobe128, on the other hand, -/// can absorb/squeeze 166 bytes before it needs to run the keccak256 permutation. -/// `136 / 166 * 6 ≈ 4.9`, which we round up to 5 gas, instead of 6 gas per word. -/// -/// The transcripts also use points on the Ristretto group for Curve25519 and require -/// scalar multiplications. Scalar multiplication is optimized through the use of the -/// Montgomery ladder for Curve25519, so this should be as fast or faster than -/// a Secp256k1 scalar multiplication. Benchmarks by XRLP support this: -/// -/// We bound the cost at that of ecrecover, which performs 3 secp256k1 -/// scalar multiplications, a point addition, plus some other computation. -/// Charging the same amount as ecrecover (3000 gas) is very conservative -/// but allows us to lower the cost later on. -/// -/// ### Pricing RNG Operations -/// The cost of initializing the `leaf_rng` comes from: -/// -/// * The Root RNG initialization requires a running hash of the transcript. The Root RNG -/// is initialized by adding 13 bytes to the transcript and then keying the rng -/// (essentially hashing) using Strobe128. -/// -/// * (optional) If personalization bytes are provided, the RNG is seeded with -/// those pers bytes -/// -/// * Each leaf RNG requires forking the `root_rng`, which involves adding -/// a 32-byte `tx_hash` and label (2 bytes) per transaction. Then a separate -/// VRF hash function is used that performs a single EC scalar multiplication -/// -/// * The leaf RNG is initialized, which involves keying the RNG based on 32 random bytes -/// from the parent RNG. -/// -/// **Filling bytes** once the RNG is initialized: -/// -/// * Filling bytes occurs by squeezing the keccak sponge. As described above, -/// take inspiration from Ethereum and charge 5 gas per 32-byte word to account for the -/// cheaper Strobe parameters. -/// -/// To calculate the base init cost of the RNG precompile, we get: -/// - 100 gas from setting up Strobe128 -/// - `(13 + len(pers) + 32 + 2 + 32) * 5 = 395 + 5 * len(pers)` gas for hashing init bytes -/// - 3000 gas for the EC scalar multiplication -/// -/// We add a 50% buffer to our gas calculations (which may be lowered in the future). -/// +/// Every call pays a flat base cost plus a per-word output cost: /// ```text -/// RNG_INIT_BASE = round(100 + 395 + 3000) = 3500 -/// fill_cost = ceil(fill_len / 32) * 5 +/// cost = RNG_BASE_COST + ceil(output_len / 32) * RNG_WORD_COST /// ``` +/// +/// The base cost (3500 gas) covers the HKDF-Extract and initial Expand rounds. +/// The per-word cost (5 gas) covers additional HMAC-SHA256 rounds for larger outputs. fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) -> PrecompileResult { // Validate input and extract parameters. validate_input_length(input.len(), MIN_INPUT_LENGTH)?; @@ -119,33 +76,28 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - let requested_output_len = requested_output_len as usize; // Compute the gas cost. - let gas_used = evmctx - .chain() - .calculate_gas_cost(&pers, requested_output_len); + let gas_used = evmctx.chain().calculate_gas_cost(requested_output_len); if gas_used > gas_limit { - return Err(PrecompileError::OutOfGas); // Changed REVM_ERROR to PrecompileError + return Err(PrecompileError::OutOfGas); } let total_gas_remaining = evmctx.chain().gas_remaining_all_frames() + gas_limit; - // Obtain kernel mode and transaction hash. - let kernel_mode = evmctx.tx().rng_mode(); let tx_hash = evmctx.tx().tx_hash(); - // Let the container update its state and produce the random bytes. + // Derive the random bytes (stateless — each call is independent). let output = evmctx - .chain_mut() - .process_rng(&pers, requested_output_len, kernel_mode, &tx_hash,total_gas_remaining) - .map_err(|e| PrecompileError::Other(e.to_string()))?; // Changed PCError to PrecompileError + .chain() + .process_rng(&pers, requested_output_len, &tx_hash, total_gas_remaining) + .map_err(|e| PrecompileError::Other(e.to_string()))?; Ok(PrecompileOutput::new(gas_used, output)) } -pub(crate) fn calculate_init_cost(pers_len: usize) -> u64 { - calc_linear_cost_u32(pers_len, RNG_INIT_BASE, STROBE128WORD) -} - -pub(crate) fn calculate_fill_cost(fill_len: usize) -> u64 { - calc_linear_cost_u32(fill_len, 0, STROBE128WORD) +/// Calculate the gas cost for an RNG precompile call. +/// Every call pays: BASE_COST + ceil(output_len / 32) * WORD_COST +pub(crate) fn calculate_gas_cost(output_len: usize) -> u64 { + let words = (output_len as u64).div_ceil(32); + RNG_BASE_COST + words * RNG_WORD_COST } // SAFETY: Indexing is validated by the length check above @@ -242,6 +194,7 @@ mod tests { ); let output = result.unwrap(); + // cost = 3500 + ceil(32/32) * 5 = 3505 assert_eq!(output.gas_used, 3505, "Should consume exactly 3505 gas"); assert!(output.bytes.len() == 32, "RNG output should be 32 bytes"); } @@ -261,9 +214,10 @@ mod tests { ); let output_with_pers = result_with_pers.unwrap(); + // cost = 3500 + ceil(32/32) * 5 = 3505 assert_eq!( - output_with_pers.gas_used, 3510, - "Should consume exactly 3510 gas" + output_with_pers.gas_used, 3505, + "Should consume exactly 3505 gas" ); assert!( output_with_pers.bytes.len() == 32, @@ -292,41 +246,44 @@ mod tests { #[test] fn test_rng_init_with_pers() { - let personalization = vec![1, 2, 3, 4]; // use 4 pers bytes, gets rounded up to one word + let personalization = vec![1, 2, 3, 4]; let (gas_limit, input, mut context, precompile) = setup_rng_test(32, Some(personalization)); let result = precompile.1(&mut context, &input.into(), gas_limit); assert!(result.is_ok(), "Should succeed with personalization"); let output = result.unwrap(); - assert_eq!(output.gas_used, 3510, "Should consume exactly 3510 gas"); + // cost = 3500 + ceil(32/32) * 5 = 3505 + assert_eq!(output.gas_used, 3505, "Should consume exactly 3505 gas"); assert!(output.bytes.len() == 32, "RNG output should be 32 bytes"); } #[test] - fn test_rng_already_initialized() { - let empty_pers = vec![0, 0, 0, 0]; // U32::ZERO.to_be_bytes_vec() + fn test_rng_second_call_pays_full_cost() { + let empty_pers = vec![0, 0, 0, 0]; let (_, input, mut context, precompile) = setup_rng_test(32, Some(empty_pers)); - // Call once to initialize the RNG + // Call once let _ = precompile.1(&mut context, &input.clone().into(), 6000); - // Make a second call with the leaf rng already initialized - let reduced_gas_limit = 500; - let result = precompile.1(&mut context, &input.into(), reduced_gas_limit); - assert!(result.is_ok(), "Should succeed with initialized RNG"); + // Second call should pay the same full cost (no caching discount) + let result = precompile.1(&mut context, &input.into(), 6000); + assert!(result.is_ok(), "Should succeed on second call"); let output = result.unwrap(); - assert_eq!(output.gas_used, 5, "Should consume exactly 5 gas"); + assert_eq!( + output.gas_used, 3505, + "Should consume exactly 3505 gas (full cost, no caching)" + ); assert!(output.bytes.len() == 32, "RNG output should be 32 bytes"); } #[test] - fn test_rng_out_of_gas_on_init() { - let empty_pers = vec![0, 0, 0, 0]; // U32::ZERO.to_be_bytes_vec() + fn test_rng_out_of_gas() { + let empty_pers = vec![0, 0, 0, 0]; let (_, input, mut context, precompile) = setup_rng_test(16, Some(empty_pers)); - let insufficient_gas = 2500; // less than the init cost + let insufficient_gas = 2500; // less than the base cost of 3500 let result = precompile.1(&mut context, &input.into(), insufficient_gas); assert!(result.is_err()); @@ -337,15 +294,12 @@ mod tests { } #[test] - fn test_rng_out_of_gas_on_fill() { - let empty_pers = vec![0, 0, 0, 0]; // U32::ZERO.to_be_bytes_vec() + fn test_rng_out_of_gas_large_output() { + let empty_pers = vec![0, 0, 0, 0]; let (_, input, mut context, precompile) = setup_rng_test(6000, Some(empty_pers)); - // Call once to initialize the RNG - let _ = precompile.1(&mut context, &input.clone().into(), 6000); - - // Make a second call with the leaf rng already initialized - let insufficient_gas = 100; + // cost = 3500 + ceil(6000/32) * 5 = 3500 + 188*5 = 3500 + 940 = 4440 + let insufficient_gas = 4000; let result = precompile.1(&mut context, &input.into(), insufficient_gas); assert!(result.is_err()); @@ -361,14 +315,11 @@ mod tests { let input_vector = vec![0x00, 0x01, 0x02]; // 3 bytes only let input = Bytes::from(input_vector); - // Use our setup function to get the context and precompile - // We can use dummy values for bytes_requested and personalization since we'll override the input let (gas_limit, _, mut context, precompile) = setup_rng_test(0, None); let result = precompile.1(&mut context, &input.into(), gas_limit); assert!(result.is_err()); - // We expect a PCError::Other complaining about input length match result.err() { Some(PrecompileError::Other(msg)) => { assert!( diff --git a/crates/seismic/src/precompiles/rng/test.rs b/crates/seismic/src/precompiles/rng/test.rs index d53c7516..3e3d56fb 100644 --- a/crates/seismic/src/precompiles/rng/test.rs +++ b/crates/seismic/src/precompiles/rng/test.rs @@ -7,7 +7,6 @@ use super::*; use domain_sep_rng::RootRng; -use rand_core::RngCore; use revm::primitives::B256; use schnorrkel::{keys::Keypair as SchnorrkelKeypair, ExpansionMode}; use std::str::FromStr; @@ -18,239 +17,122 @@ fn hex_to_hash_bytes(input: &str) -> B256 { #[test] fn test_rng_basic() { + // First derivation with empty pers let root_rng = RootRng::test_default(); + let bytes1 = root_rng.derive_bytes(&[], 32); - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1_1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1_1); - - assert_ne!(bytes1, bytes1_1, "rng should apply domain separation"); - - // Create second root RNG using the same context so the ephemeral key is shared. - let root_rng = RootRng::test_default(); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes2 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes2); - - assert_eq!(bytes1, bytes2, "rng should be deterministic"); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes2_1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes2_1); - - assert_ne!(bytes2, bytes2_1, "rng should apply domain separation"); - assert_eq!(bytes1_1, bytes2_1, "rng should be deterministic"); - - // Create third root RNG using the same context, but with different personalization. - let root_rng = RootRng::test_default(); + // Same RNG, same pers — should produce the same output (stateless derivation) + let bytes1_again = root_rng.derive_bytes(&[], 32); + assert_eq!( + bytes1, bytes1_again, + "same inputs should produce same output" + ); - let mut leaf_rng = root_rng.fork(b"domsep"); - let mut bytes3 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes3); + // Create second root RNG using the same keypair — should produce the same output + let root_rng2 = RootRng::test_default(); + let bytes2 = root_rng2.derive_bytes(&[], 32); + assert_eq!( + bytes1, bytes2, + "rng should be deterministic across instances" + ); - assert_ne!(bytes2, bytes3, "rng should apply domain separation"); + // Different personalization should produce different output + let bytes3 = root_rng.derive_bytes(b"domsep", 32); + assert_ne!( + bytes1, bytes3, + "different pers should produce different output" + ); - // Create another root RNG using the same context, but with different history. - let root_rng = RootRng::test_default(); - root_rng.append_tx(&hex_to_hash_bytes( + // Appending a tx hash should change the output + let mut root_rng3 = RootRng::test_default(); + root_rng3.append_tx(&hex_to_hash_bytes( "0000000000000000000000000000000000000000000000000000000000000001", )); + let bytes4 = root_rng3.derive_bytes(&[], 32); + assert_ne!(bytes1, bytes4, "tx hash should change output"); - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes4 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes4); - - assert_ne!(bytes2, bytes4, "rng should apply domain separation"); - - // Create another root RNG using the same context, but with different history. - let root_rng = RootRng::test_default(); - root_rng.append_tx(&hex_to_hash_bytes( + // Different tx hash should produce different output + let mut root_rng4 = RootRng::test_default(); + root_rng4.append_tx(&hex_to_hash_bytes( "0000000000000000000000000000000000000000000000000000000000000002", )); + let bytes5 = root_rng4.derive_bytes(&[], 32); + assert_ne!( + bytes4, bytes5, + "different tx hash should produce different output" + ); - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes5 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes5); - - assert_ne!(bytes4, bytes5, "rng should apply domain separation"); - - // Create another root RNG using the same context, but with same history as four. - let root_rng = RootRng::test_default(); - root_rng.append_tx(&hex_to_hash_bytes( - "0000000000000000000000000000000000000000000000000000000000000001", - )); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes6 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes6); - - assert_eq!(bytes4, bytes6, "rng should be deterministic"); - - // Create another root RNG using the same context, but with different history. - let root_rng = RootRng::test_default(); - root_rng.append_tx(&hex_to_hash_bytes( + // Same tx hash as root_rng3 should produce the same output + let mut root_rng5 = RootRng::test_default(); + root_rng5.append_tx(&hex_to_hash_bytes( "0000000000000000000000000000000000000000000000000000000000000001", )); - root_rng.append_tx(&hex_to_hash_bytes( - "0000000000000000000000000000000000000000000000000000000000000002", - )); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes7 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes7); - - assert_ne!(bytes4, bytes7, "rng should apply domain separation"); + let bytes6 = root_rng5.derive_bytes(&[], 32); + assert_eq!(bytes4, bytes6, "same tx hash should be deterministic"); - // Create another root RNG using the same context, but with different init point. - let root_rng = RootRng::test_default(); - root_rng.append_tx(&hex_to_hash_bytes( + // Multiple tx hashes should produce different output than single + let mut root_rng6 = RootRng::test_default(); + root_rng6.append_tx(&hex_to_hash_bytes( "0000000000000000000000000000000000000000000000000000000000000001", )); - let _ = root_rng.fork(&[]); // Force init. - root_rng.append_tx(&hex_to_hash_bytes( + root_rng6.append_tx(&hex_to_hash_bytes( "0000000000000000000000000000000000000000000000000000000000000002", )); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes8 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes8); - - assert_ne!(bytes7, bytes8, "rng should apply domain separation"); - assert_ne!(bytes6, bytes8, "rng should apply domain separation"); + let bytes7 = root_rng6.derive_bytes(&[], 32); + assert_ne!(bytes4, bytes7, "multiple tx hashes should change output"); } #[test] -fn test_rng_local_entropy() { - let eph_rng_keypair: SchnorrkelKeypair = schnorrkel::MiniSecretKey::generate() - .expand(ExpansionMode::Uniform) - .into(); - let root_rng = RootRng::new(eph_rng_keypair.clone()); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); +fn test_rng_gas_domain_separation() { + let mut root_rng1 = RootRng::test_default(); + root_rng1.append_tx(&B256::from([1u8; 32])); + root_rng1.append_gas_left(1000); + let bytes1 = root_rng1.derive_bytes(&[], 32); - // Create second root RNG using the same context, but mix in local entropy. - let root_rng = RootRng::test_default(); - root_rng.append_local_entropy(); + let mut root_rng2 = RootRng::test_default(); + root_rng2.append_tx(&B256::from([1u8; 32])); + root_rng2.append_gas_left(2000); + let bytes2 = root_rng2.derive_bytes(&[], 32); - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes2 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes2); - - assert_ne!(bytes1, bytes2, "rng should apply domain separation"); + assert_ne!( + bytes1, bytes2, + "different gas_left should produce different output" + ); } #[test] -fn test_rng_parent_fork_propagation() { - let eph_rng_keypair: SchnorrkelKeypair = schnorrkel::MiniSecretKey::generate() +fn test_rng_different_keys_different_output() { + let keypair1: SchnorrkelKeypair = schnorrkel::MiniSecretKey::generate() + .expand(ExpansionMode::Uniform) + .into(); + let keypair2: SchnorrkelKeypair = schnorrkel::MiniSecretKey::generate() .expand(ExpansionMode::Uniform) .into(); - let root_rng = RootRng::new(eph_rng_keypair.clone()); - - let mut leaf_rng = root_rng.fork(b"a"); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng = root_rng.fork(b"a"); - let mut bytes1_1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1_1); - - // Create second root RNG. - let root_rng = RootRng::test_default(); - let mut leaf_rng = root_rng.fork(b"b"); - let mut bytes2 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes2); + let root_rng1 = RootRng::new(keypair1); + let root_rng2 = RootRng::new(keypair2); - let mut leaf_rng = root_rng.fork(b"a"); - let mut bytes2_1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes2_1); + let bytes1 = root_rng1.derive_bytes(&[], 32); + let bytes2 = root_rng2.derive_bytes(&[], 32); assert_ne!( - bytes1_1, bytes2_1, - "forks should propagate domain separator to parent" + bytes1, bytes2, + "different keys should produce different output" ); } #[test] -fn test_clone_rng_before_init() { +fn test_large_output() { let root_rng = RootRng::test_default(); - // clone and test leaves are the same - let root_rng_2 = root_rng.clone(); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng_2 = root_rng_2.fork(&[]); - let mut bytes2 = [0u8; 32]; - leaf_rng_2.fill_bytes(&mut bytes2); - - assert_eq!(bytes1, bytes2, "rng should be deterministic"); -} - -#[test] -fn test_clone_rng_after_init() { - let root_rng = RootRng::test_default(); + // Request more than the HKDF single-expand limit (8160 bytes) + let large_output = root_rng.derive_bytes(b"large", 10000); + assert_eq!(large_output.len(), 10000); - // fork - root_rng.append_tx(&B256::from([1u8; 32])); - let _ = root_rng.fork(&[]); - - // clone and test rng is same - let root_rng_2 = root_rng.clone(); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng_2 = root_rng_2.fork(&[]); - let mut bytes2 = [0u8; 32]; - leaf_rng_2.fill_bytes(&mut bytes2); - - assert_eq!(bytes1, bytes2, "rng should be deterministic"); - - let root_rng_3 = root_rng.clone(); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng_3 = root_rng_3.fork(&[]); - let mut bytes3 = [0u8; 32]; - leaf_rng_3.fill_bytes(&mut bytes3); - - assert_eq!(bytes1, bytes3, "rng should be deterministic"); -} - -#[test] -fn test_clone_after_local_entropy() { - let eph_rng_keypair: SchnorrkelKeypair = schnorrkel::MiniSecretKey::generate() - .expand(ExpansionMode::Uniform) - .into(); - let root_rng = RootRng::new(eph_rng_keypair.clone()); - - // simulate some initial transactions with local entropy - let _ = root_rng.fork(&[]); - root_rng.append_local_entropy(); - let _ = root_rng.fork(&[]); - root_rng.append_local_entropy(); - - // clone and test rng is same - let root_rng_2 = root_rng.clone(); - - let mut leaf_rng = root_rng.fork(&[]); - let mut bytes1 = [0u8; 32]; - leaf_rng.fill_bytes(&mut bytes1); - - let mut leaf_rng_2 = root_rng_2.fork(&[]); - let mut bytes2 = [0u8; 32]; - leaf_rng_2.fill_bytes(&mut bytes2); + // Verify determinism for large outputs + let large_output_2 = root_rng.derive_bytes(b"large", 10000); + assert_eq!( + large_output, large_output_2, + "large output should be deterministic" + ); } diff --git a/crates/seismic/src/transaction/abstraction.rs b/crates/seismic/src/transaction/abstraction.rs index 8c0eb8b4..f6ec946c 100644 --- a/crates/seismic/src/transaction/abstraction.rs +++ b/crates/seismic/src/transaction/abstraction.rs @@ -5,23 +5,10 @@ use revm::{ primitives::{Address, Bytes, TxKind, B256, U256}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// Indicates the runtime context for the kernel. -/// Use `Simulation` for endpoints (like eth_call) that need unique entropy, -/// and `Execution` for normal transaction execution (used for both tests and production). -pub enum RngMode { - Simulation, - Execution, -} - #[auto_impl(&, &mut, Box, Arc)] pub trait SeismicTxTr: Transaction { /// tx hash of the transaction fn tx_hash(&self) -> B256; - - /// rng mode for this transaction - fn rng_mode(&self) -> RngMode; } #[derive(Clone, Debug, PartialEq, Eq)] @@ -30,7 +17,6 @@ pub struct SeismicTransaction { pub base: T, /// tx hash of the transaction. Used for domain separation in the RNG. pub tx_hash: B256, - pub rng_mode: RngMode, } impl SeismicTransaction { @@ -38,7 +24,6 @@ impl SeismicTransaction { Self { base, tx_hash: B256::ZERO, - rng_mode: RngMode::Execution, } } @@ -46,11 +31,6 @@ impl SeismicTransaction { self.tx_hash = tx_hash; self } - - pub fn with_rng_mode(mut self, rng_mode: RngMode) -> Self { - self.rng_mode = rng_mode; - self - } } impl std::ops::Deref for SeismicTransaction { @@ -77,7 +57,6 @@ impl Default for SeismicTransaction { Self { base: TxEnv::default(), tx_hash: B256::ZERO, - rng_mode: RngMode::Execution, } } } @@ -165,8 +144,4 @@ impl SeismicTxTr for SeismicTransaction { fn tx_hash(&self) -> B256 { self.tx_hash } - - fn rng_mode(&self) -> RngMode { - self.rng_mode - } } From 327488fe577b210a1ee40ec46f1c6f7e67d3039e Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Mon, 16 Feb 2026 16:07:10 -0500 Subject: [PATCH 3/8] make SeismicContext with random key explicit + make sementic tests determnistic --- bins/revme/src/cmd/semantics/evm_handler.rs | 10 +++++----- bins/revme/src/cmd/statetest/runner.rs | 6 +++--- crates/seismic/src/api/default_ctx.rs | 8 ++++---- crates/seismic/src/evm.rs | 2 +- crates/seismic/src/handler.rs | 4 ++-- crates/seismic/src/precompiles/rng/precompile.rs | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/bins/revme/src/cmd/semantics/evm_handler.rs b/bins/revme/src/cmd/semantics/evm_handler.rs index 7871f1ad..a6951c79 100644 --- a/bins/revme/src/cmd/semantics/evm_handler.rs +++ b/bins/revme/src/cmd/semantics/evm_handler.rs @@ -7,7 +7,7 @@ use revm::{ primitives::{Address, Bytes, FixedBytes, Log, TxKind, U256}, Context, DatabaseCommit, DatabaseRef, ExecuteEvm, InspectEvm, MainBuilder, MainContext, }; -use seismic_revm::{DefaultSeismicContext, SeismicBuilder}; +use seismic_revm::{DefaultSeismicContext, SeismicBuilder, precompiles::rng::domain_sep_rng::SchnorrkelKeypair}; use std::str::FromStr; use crate::cmd::semantics::{test_cases::TestStep, utils::verify_emitted_events}; @@ -126,7 +126,7 @@ impl EvmExecutor { .map_or(0, |account| account.nonce); let deploy_out = if self.evm_version == EVMVersion::Mercury { if trace { - let mut evm = Context::seismic() + let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) .with_db(self.db.clone()) .modify_tx_chained(|tx| { tx.base.caller = self.config.caller; @@ -143,7 +143,7 @@ impl EvmExecutor { Errors::EVMError(format!("DEPLOY transaction error: {:?}", err.to_string())) })? } else { - let mut evm = Context::seismic() + let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) .with_db(self.db.clone()) .modify_tx_chained(|tx| { tx.base.caller = self.config.caller; @@ -243,7 +243,7 @@ impl EvmExecutor { .map_or(0, |account| account.nonce); let out = if self.evm_version == EVMVersion::Mercury { if trace { - let mut evm = Context::seismic() + let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) .with_db(self.db.clone()) .modify_tx_chained(|tx| { tx.base.caller = self.config.caller; @@ -279,7 +279,7 @@ impl EvmExecutor { )) })? } else { - let mut evm = Context::seismic() + let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) .with_db(self.db.clone()) .modify_tx_chained(|tx| { tx.base.caller = self.config.caller; diff --git a/bins/revme/src/cmd/statetest/runner.rs b/bins/revme/src/cmd/statetest/runner.rs index 56fcbd26..7179c98a 100644 --- a/bins/revme/src/cmd/statetest/runner.rs +++ b/bins/revme/src/cmd/statetest/runner.rs @@ -17,7 +17,7 @@ use revm::{ }; use seismic_revm::{ DefaultSeismicContext, SeismicBuilder, SeismicHaltReason, SeismicHaltReason as HaltReason, - SeismicSpecId as SpecId, SeismicTransaction, + SeismicSpecId as SpecId, SeismicTransaction, precompiles::rng::domain_sep_rng::SchnorrkelKeypair, }; use serde_json::json; use statetest_types::{SpecName, Test, TestSuite, TestUnit}; @@ -426,7 +426,7 @@ fn execute_single_test(ctx: TestExecutionContext) -> Result<(), TestErrorKind> { .with_bundle_update() .build(); - let evm_context = Context::seismic() + let evm_context = Context::seismic_with_random_rng_key() .with_block(ctx.block) .with_tx(ctx.tx) .with_cfg(ctx.cfg) @@ -476,7 +476,7 @@ fn debug_failed_test(ctx: DebugContext) { .with_bundle_update() .build(); - let mut evm = Context::seismic() + let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) .with_db(&mut state) .with_block(ctx.block) .with_tx(ctx.tx) diff --git a/crates/seismic/src/api/default_ctx.rs b/crates/seismic/src/api/default_ctx.rs index b3cd5665..63357a1c 100644 --- a/crates/seismic/src/api/default_ctx.rs +++ b/crates/seismic/src/api/default_ctx.rs @@ -17,14 +17,14 @@ pub type SeismicContext = Context< /// Trait that allows for a default context to be created. pub trait DefaultSeismicContext { - /// Create a default context. - fn seismic() -> SeismicContext; + /// Create a context that uses a random rng key for precompile everytime. + fn seismic_with_random_rng_key() -> SeismicContext; /// Create a context with a specific RNG keypair. fn seismic_with_rng_key(rng_keypair: schnorrkel::Keypair) -> SeismicContext; } impl DefaultSeismicContext for SeismicContext { - fn seismic() -> Self { + fn seismic_with_random_rng_key() -> Self { Context::mainnet() .with_tx(SeismicTransaction::default()) .with_cfg(CfgEnv::new_with_spec(SeismicSpecId::MERCURY)) @@ -47,7 +47,7 @@ mod test { #[test] fn default_run_seismic() { - let ctx = Context::seismic(); + let ctx = Context::seismic_with_random_rng_key(); // convert to seismic context let mut evm = ctx.build_seismic_evm_with_inspector(NoOpInspector {}); // execute diff --git a/crates/seismic/src/evm.rs b/crates/seismic/src/evm.rs index f0f519c9..75644360 100644 --- a/crates/seismic/src/evm.rs +++ b/crates/seismic/src/evm.rs @@ -268,7 +268,7 @@ mod tests { fn deploy_contract_with_bytecode( bytecode: Bytes, ) -> anyhow::Result<(SeismicContext, Address)> { - let ctx = Context::seismic() + let ctx = Context::seismic_with_random_rng_key() .modify_tx_chained(|tx| { tx.base.kind = TxKind::Create; tx.base.data = bytecode.clone(); diff --git a/crates/seismic/src/handler.rs b/crates/seismic/src/handler.rs index adc4e44e..a9ce4985 100644 --- a/crates/seismic/src/handler.rs +++ b/crates/seismic/src/handler.rs @@ -163,7 +163,7 @@ mod tests { #[test] fn test_revert_gas() { - let ctx = Context::seismic().modify_tx_chained(|tx| { + let ctx = Context::seismic_with_random_rng_key().modify_tx_chained(|tx| { tx.base.gas_limit = 100; }); @@ -175,7 +175,7 @@ mod tests { #[test] fn test_fatal_external_error_gas() { - let ctx = Context::seismic().modify_tx_chained(|tx| { + let ctx = Context::seismic_with_random_rng_key().modify_tx_chained(|tx| { tx.base.gas_limit = 100; }); diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index d4dec400..c734073a 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -175,7 +175,7 @@ mod tests { // Setup transaction and context let tx = SeismicTransaction::default().with_tx_hash(B256::from([0u8; 32])); - let context = Context::seismic().with_tx(tx); + let context = Context::seismic_with_random_rng_key().with_tx(tx); // Get precompile function let precompile = rng_precompile::>; From 2b02960b6fd0e3121f2b30dfd5acf0389e4589f0 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Mon, 16 Feb 2026 17:23:32 -0500 Subject: [PATCH 4/8] fix rng precompile gas cost --- crates/seismic/src/chain/rng_container.rs | 5 +--- crates/seismic/src/chain/seismic_chain.rs | 6 +---- crates/seismic/src/evm.rs | 2 +- .../seismic/src/precompiles/rng/precompile.rs | 25 ++++++++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/seismic/src/chain/rng_container.rs b/crates/seismic/src/chain/rng_container.rs index 0bac7750..ae926368 100644 --- a/crates/seismic/src/chain/rng_container.rs +++ b/crates/seismic/src/chain/rng_container.rs @@ -1,4 +1,4 @@ -use crate::precompiles::rng::{domain_sep_rng::RootRng, precompile::calculate_gas_cost}; +use crate::precompiles::rng::{domain_sep_rng::RootRng}; use revm::{ precompile::PrecompileError, primitives::{Bytes, B256}, @@ -35,6 +35,3 @@ pub fn derive_rng_output( Ok(Bytes::from(rng_bytes)) } -pub fn rng_gas_cost(requested_output_len: usize) -> u64 { - calculate_gas_cost(requested_output_len) -} diff --git a/crates/seismic/src/chain/seismic_chain.rs b/crates/seismic/src/chain/seismic_chain.rs index 5d3b9e30..7a68f49b 100644 --- a/crates/seismic/src/chain/seismic_chain.rs +++ b/crates/seismic/src/chain/seismic_chain.rs @@ -3,7 +3,7 @@ use revm::{ primitives::{Bytes, B256}, }; -use super::rng_container::{derive_rng_output, rng_gas_cost}; +use super::rng_container::{derive_rng_output}; #[derive(Clone, Debug, Default)] pub struct SeismicChain { @@ -39,10 +39,6 @@ impl SeismicChain { self.gas_remaining_all_frames = gas; } - pub fn calculate_gas_cost(&self, requested_output_len: usize) -> u64 { - rng_gas_cost(requested_output_len) - } - pub fn process_rng( &self, pers: &[u8], diff --git a/crates/seismic/src/evm.rs b/crates/seismic/src/evm.rs index 75644360..fa783125 100644 --- a/crates/seismic/src/evm.rs +++ b/crates/seismic/src/evm.rs @@ -415,7 +415,7 @@ mod tests { let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas(spec.into(), &input[..], false, 0, 0, 0); - let total_gas = initial_gas + calculate_gas_cost(bytes_requested as usize); + let total_gas = initial_gas + calculate_gas_cost(bytes_requested as usize, personalization.len()); Context::seismic_with_rng_key(keypair) .modify_tx_chained(|tx| { diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index c734073a..c1d13653 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -76,7 +76,7 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - let requested_output_len = requested_output_len as usize; // Compute the gas cost. - let gas_used = evmctx.chain().calculate_gas_cost(requested_output_len); + let gas_used = calculate_gas_cost(requested_output_len, pers.len()); if gas_used > gas_limit { return Err(PrecompileError::OutOfGas); } @@ -95,9 +95,16 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - /// Calculate the gas cost for an RNG precompile call. /// Every call pays: BASE_COST + ceil(output_len / 32) * WORD_COST -pub(crate) fn calculate_gas_cost(output_len: usize) -> u64 { - let words = (output_len as u64).div_ceil(32); - RNG_BASE_COST + words * RNG_WORD_COST +pub(crate) fn calculate_gas_cost(pers_len: usize, output_len: usize) -> u64 { + calculate_init_cost(pers_len) + calculate_fill_cost(output_len) +} + +fn calculate_init_cost(pers_len: usize) -> u64 { + (pers_len as u64).div_ceil(32).saturating_mul(RNG_WORD_COST).saturating_add(RNG_BASE_COST) +} + +fn calculate_fill_cost(fill_len: usize) -> u64 { + (fill_len as u64).div_ceil(32).saturating_mul(RNG_WORD_COST) } // SAFETY: Indexing is validated by the length check above @@ -216,8 +223,8 @@ mod tests { let output_with_pers = result_with_pers.unwrap(); // cost = 3500 + ceil(32/32) * 5 = 3505 assert_eq!( - output_with_pers.gas_used, 3505, - "Should consume exactly 3505 gas" + output_with_pers.gas_used, 3510, + "Should consume exactly 3510 gas" ); assert!( output_with_pers.bytes.len() == 32, @@ -254,7 +261,7 @@ mod tests { let output = result.unwrap(); // cost = 3500 + ceil(32/32) * 5 = 3505 - assert_eq!(output.gas_used, 3505, "Should consume exactly 3505 gas"); + assert_eq!(output.gas_used, 3510, "Should consume exactly 3510 gas"); assert!(output.bytes.len() == 32, "RNG output should be 32 bytes"); } @@ -272,8 +279,8 @@ mod tests { let output = result.unwrap(); assert_eq!( - output.gas_used, 3505, - "Should consume exactly 3505 gas (full cost, no caching)" + output.gas_used, 3510, + "Should consume exactly 3510 gas (full cost, no caching)" ); assert!(output.bytes.len() == 32, "RNG output should be 32 bytes"); } From a0be18f14278482093c8b1df1b5177632feef40b Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Mon, 16 Feb 2026 17:34:46 -0500 Subject: [PATCH 5/8] fix rebase --- crates/seismic/src/precompiles/rng/precompile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index c1d13653..bb2111f0 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -96,7 +96,7 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - /// Calculate the gas cost for an RNG precompile call. /// Every call pays: BASE_COST + ceil(output_len / 32) * WORD_COST pub(crate) fn calculate_gas_cost(pers_len: usize, output_len: usize) -> u64 { - calculate_init_cost(pers_len) + calculate_fill_cost(output_len) + calculate_init_cost(pers_len).saturating_add(calculate_fill_cost(output_len)) } fn calculate_init_cost(pers_len: usize) -> u64 { From 71d7df14d199cc1cd4247c50aee36de215286c82 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Mon, 16 Feb 2026 17:36:43 -0500 Subject: [PATCH 6/8] fmt --- bins/revme/src/cmd/semantics/evm_handler.rs | 164 +++++++++--------- bins/revme/src/cmd/statetest/runner.rs | 18 +- crates/seismic/src/chain/rng_container.rs | 3 +- crates/seismic/src/chain/seismic_chain.rs | 2 +- crates/seismic/src/evm.rs | 3 +- .../seismic/src/precompiles/rng/precompile.rs | 7 +- 6 files changed, 104 insertions(+), 93 deletions(-) diff --git a/bins/revme/src/cmd/semantics/evm_handler.rs b/bins/revme/src/cmd/semantics/evm_handler.rs index a6951c79..51039662 100644 --- a/bins/revme/src/cmd/semantics/evm_handler.rs +++ b/bins/revme/src/cmd/semantics/evm_handler.rs @@ -7,7 +7,9 @@ use revm::{ primitives::{Address, Bytes, FixedBytes, Log, TxKind, U256}, Context, DatabaseCommit, DatabaseRef, ExecuteEvm, InspectEvm, MainBuilder, MainContext, }; -use seismic_revm::{DefaultSeismicContext, SeismicBuilder, precompiles::rng::domain_sep_rng::SchnorrkelKeypair}; +use seismic_revm::{ + precompiles::rng::domain_sep_rng::SchnorrkelKeypair, DefaultSeismicContext, SeismicBuilder, +}; use std::str::FromStr; use crate::cmd::semantics::{test_cases::TestStep, utils::verify_emitted_events}; @@ -126,34 +128,36 @@ impl EvmExecutor { .map_or(0, |account| account.nonce); let deploy_out = if self.evm_version == EVMVersion::Mercury { if trace { - let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) - .with_db(self.db.clone()) - .modify_tx_chained(|tx| { - tx.base.caller = self.config.caller; - tx.base.kind = TxKind::Create; - tx.base.data = deploy_data.clone(); - tx.base.value = value; - tx.base.nonce = nonce; - }) - .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) - .build_seismic_evm_with_inspector( - TracerEip3155::new_stdout().without_summary(), - ); + let mut evm = Context::seismic_with_rng_key( + SchnorrkelKeypair::from_bytes(&[0; 96]).expect("safe"), + ) + .with_db(self.db.clone()) + .modify_tx_chained(|tx| { + tx.base.caller = self.config.caller; + tx.base.kind = TxKind::Create; + tx.base.data = deploy_data.clone(); + tx.base.value = value; + tx.base.nonce = nonce; + }) + .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) + .build_seismic_evm_with_inspector(TracerEip3155::new_stdout().without_summary()); evm.inspect_tx(evm.tx.clone()).map_err(|err| { Errors::EVMError(format!("DEPLOY transaction error: {:?}", err.to_string())) })? } else { - let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) - .with_db(self.db.clone()) - .modify_tx_chained(|tx| { - tx.base.caller = self.config.caller; - tx.base.kind = TxKind::Create; - tx.base.data = deploy_data.clone(); - tx.base.value = value; - tx.base.nonce = nonce; - }) - .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) - .build_seismic_evm(); + let mut evm = Context::seismic_with_rng_key( + SchnorrkelKeypair::from_bytes(&[0; 96]).expect("safe"), + ) + .with_db(self.db.clone()) + .modify_tx_chained(|tx| { + tx.base.caller = self.config.caller; + tx.base.kind = TxKind::Create; + tx.base.data = deploy_data.clone(); + tx.base.value = value; + tx.base.nonce = nonce; + }) + .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) + .build_seismic_evm(); evm.replay().map_err(|err| { Errors::EVMError(format!("DEPLOY transaction error: {:?}", err.to_string())) })? @@ -243,34 +247,34 @@ impl EvmExecutor { .map_or(0, |account| account.nonce); let out = if self.evm_version == EVMVersion::Mercury { if trace { - let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) - .with_db(self.db.clone()) - .modify_tx_chained(|tx| { - tx.base.caller = self.config.caller; - tx.base.kind = TxKind::Call(self.config.env_contract_address); - tx.base.data = input_data.clone(); - tx.base.value = value; - if self.evm_version >= EVMVersion::Cancun { - tx.base.blob_hashes = self.config.blob_hashes.clone(); - tx.base.max_fee_per_blob_gas = self.config.max_blob_fee; - } - tx.base.gas_limit = self.config.gas_limit; - tx.base.gas_price = self.config.gas_price; - tx.base.gas_priority_fee = self.config.gas_priority_fee; - tx.base.nonce = nonce; - }) - .modify_block_chained(|block| { - block.prevrandao = Some(self.config.block_prevrandao); - block.difficulty = self.config.block_difficulty.into(); - block.gas_limit = self.config.block_gas_limit; - block.basefee = self.config.block_basefee; - block.number = self.config.block_number; - block.timestamp = self.config.timestamp; - }) - .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) - .build_seismic_evm_with_inspector(TracerEip3155::new(Box::new( - std::io::stdout(), - ))); + let mut evm = Context::seismic_with_rng_key( + SchnorrkelKeypair::from_bytes(&[0; 96]).expect("safe"), + ) + .with_db(self.db.clone()) + .modify_tx_chained(|tx| { + tx.base.caller = self.config.caller; + tx.base.kind = TxKind::Call(self.config.env_contract_address); + tx.base.data = input_data.clone(); + tx.base.value = value; + if self.evm_version >= EVMVersion::Cancun { + tx.base.blob_hashes = self.config.blob_hashes.clone(); + tx.base.max_fee_per_blob_gas = self.config.max_blob_fee; + } + tx.base.gas_limit = self.config.gas_limit; + tx.base.gas_price = self.config.gas_price; + tx.base.gas_priority_fee = self.config.gas_priority_fee; + tx.base.nonce = nonce; + }) + .modify_block_chained(|block| { + block.prevrandao = Some(self.config.block_prevrandao); + block.difficulty = self.config.block_difficulty.into(); + block.gas_limit = self.config.block_gas_limit; + block.basefee = self.config.block_basefee; + block.number = self.config.block_number; + block.timestamp = self.config.timestamp; + }) + .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) + .build_seismic_evm_with_inspector(TracerEip3155::new(Box::new(std::io::stdout()))); evm.inspect_tx(evm.tx.clone()).map_err(|err| { Errors::EVMError(format!( "EVM transaction error: {:?}, for the file: {:?}", @@ -279,32 +283,34 @@ impl EvmExecutor { )) })? } else { - let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) - .with_db(self.db.clone()) - .modify_tx_chained(|tx| { - tx.base.caller = self.config.caller; - tx.base.kind = TxKind::Call(self.config.env_contract_address); - tx.base.data = input_data.clone(); - tx.base.value = value; - if self.evm_version >= EVMVersion::Cancun { - tx.base.blob_hashes = self.config.blob_hashes.clone(); - tx.base.max_fee_per_blob_gas = self.config.max_blob_fee; - } - tx.base.gas_limit = self.config.gas_limit; - tx.base.gas_price = self.config.gas_price; - tx.base.nonce = nonce; - tx.base.gas_priority_fee = self.config.gas_priority_fee; - }) - .modify_block_chained(|block| { - block.prevrandao = Some(self.config.block_prevrandao); - block.difficulty = self.config.block_difficulty.into(); - block.gas_limit = self.config.block_gas_limit; - block.basefee = self.config.block_basefee; - block.number = self.config.block_number; - block.timestamp = self.config.timestamp; - }) - .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) - .build_seismic_evm(); + let mut evm = Context::seismic_with_rng_key( + SchnorrkelKeypair::from_bytes(&[0; 96]).expect("safe"), + ) + .with_db(self.db.clone()) + .modify_tx_chained(|tx| { + tx.base.caller = self.config.caller; + tx.base.kind = TxKind::Call(self.config.env_contract_address); + tx.base.data = input_data.clone(); + tx.base.value = value; + if self.evm_version >= EVMVersion::Cancun { + tx.base.blob_hashes = self.config.blob_hashes.clone(); + tx.base.max_fee_per_blob_gas = self.config.max_blob_fee; + } + tx.base.gas_limit = self.config.gas_limit; + tx.base.gas_price = self.config.gas_price; + tx.base.nonce = nonce; + tx.base.gas_priority_fee = self.config.gas_priority_fee; + }) + .modify_block_chained(|block| { + block.prevrandao = Some(self.config.block_prevrandao); + block.difficulty = self.config.block_difficulty.into(); + block.gas_limit = self.config.block_gas_limit; + block.basefee = self.config.block_basefee; + block.number = self.config.block_number; + block.timestamp = self.config.timestamp; + }) + .modify_cfg_chained(|cfg| cfg.spec = self.evm_version.to_seismic_spec_id()) + .build_seismic_evm(); evm.replay().map_err(|err| { Errors::EVMError(format!( "EVM transaction error: {:?}, for the file: {:?}", diff --git a/bins/revme/src/cmd/statetest/runner.rs b/bins/revme/src/cmd/statetest/runner.rs index 7179c98a..a421920c 100644 --- a/bins/revme/src/cmd/statetest/runner.rs +++ b/bins/revme/src/cmd/statetest/runner.rs @@ -16,8 +16,9 @@ use revm::{ Context, ExecuteCommitEvm, }; use seismic_revm::{ - DefaultSeismicContext, SeismicBuilder, SeismicHaltReason, SeismicHaltReason as HaltReason, - SeismicSpecId as SpecId, SeismicTransaction, precompiles::rng::domain_sep_rng::SchnorrkelKeypair, + precompiles::rng::domain_sep_rng::SchnorrkelKeypair, DefaultSeismicContext, SeismicBuilder, + SeismicHaltReason, SeismicHaltReason as HaltReason, SeismicSpecId as SpecId, + SeismicTransaction, }; use serde_json::json; use statetest_types::{SpecName, Test, TestSuite, TestUnit}; @@ -476,12 +477,13 @@ fn debug_failed_test(ctx: DebugContext) { .with_bundle_update() .build(); - let mut evm = Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0;96]).expect("safe")) - .with_db(&mut state) - .with_block(ctx.block) - .with_tx(ctx.tx) - .with_cfg(ctx.cfg) - .build_seismic_evm_with_inspector(TracerEip3155::buffered(stderr()).without_summary()); + let mut evm = + Context::seismic_with_rng_key(SchnorrkelKeypair::from_bytes(&[0; 96]).expect("safe")) + .with_db(&mut state) + .with_block(ctx.block) + .with_tx(ctx.tx) + .with_cfg(ctx.cfg) + .build_seismic_evm_with_inspector(TracerEip3155::buffered(stderr()).without_summary()); let exec_result = evm.inspect_tx_commit(ctx.tx); diff --git a/crates/seismic/src/chain/rng_container.rs b/crates/seismic/src/chain/rng_container.rs index ae926368..f0287c79 100644 --- a/crates/seismic/src/chain/rng_container.rs +++ b/crates/seismic/src/chain/rng_container.rs @@ -1,4 +1,4 @@ -use crate::precompiles::rng::{domain_sep_rng::RootRng}; +use crate::precompiles::rng::domain_sep_rng::RootRng; use revm::{ precompile::PrecompileError, primitives::{Bytes, B256}, @@ -34,4 +34,3 @@ pub fn derive_rng_output( let rng_bytes = rng.derive_bytes(pers, requested_output_len); Ok(Bytes::from(rng_bytes)) } - diff --git a/crates/seismic/src/chain/seismic_chain.rs b/crates/seismic/src/chain/seismic_chain.rs index 7a68f49b..5b3f2f92 100644 --- a/crates/seismic/src/chain/seismic_chain.rs +++ b/crates/seismic/src/chain/seismic_chain.rs @@ -3,7 +3,7 @@ use revm::{ primitives::{Bytes, B256}, }; -use super::rng_container::{derive_rng_output}; +use super::rng_container::derive_rng_output; #[derive(Clone, Debug, Default)] pub struct SeismicChain { diff --git a/crates/seismic/src/evm.rs b/crates/seismic/src/evm.rs index fa783125..7ee8ca37 100644 --- a/crates/seismic/src/evm.rs +++ b/crates/seismic/src/evm.rs @@ -415,7 +415,8 @@ mod tests { let InitialAndFloorGas { initial_gas, .. } = calculate_initial_tx_gas(spec.into(), &input[..], false, 0, 0, 0); - let total_gas = initial_gas + calculate_gas_cost(bytes_requested as usize, personalization.len()); + let total_gas = + initial_gas + calculate_gas_cost(bytes_requested as usize, personalization.len()); Context::seismic_with_rng_key(keypair) .modify_tx_chained(|tx| { diff --git a/crates/seismic/src/precompiles/rng/precompile.rs b/crates/seismic/src/precompiles/rng/precompile.rs index bb2111f0..855d2033 100644 --- a/crates/seismic/src/precompiles/rng/precompile.rs +++ b/crates/seismic/src/precompiles/rng/precompile.rs @@ -95,12 +95,15 @@ fn rng(evmctx: &mut CTX, input: &Bytes, gas_limit: u64) - /// Calculate the gas cost for an RNG precompile call. /// Every call pays: BASE_COST + ceil(output_len / 32) * WORD_COST -pub(crate) fn calculate_gas_cost(pers_len: usize, output_len: usize) -> u64 { +pub(crate) fn calculate_gas_cost(pers_len: usize, output_len: usize) -> u64 { calculate_init_cost(pers_len).saturating_add(calculate_fill_cost(output_len)) } fn calculate_init_cost(pers_len: usize) -> u64 { - (pers_len as u64).div_ceil(32).saturating_mul(RNG_WORD_COST).saturating_add(RNG_BASE_COST) + (pers_len as u64) + .div_ceil(32) + .saturating_mul(RNG_WORD_COST) + .saturating_add(RNG_BASE_COST) } fn calculate_fill_cost(fill_len: usize) -> u64 { From 071f6a3bb1b8b6f3d701a9bcfdbc39dff3296861 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Tue, 17 Feb 2026 13:09:36 -0500 Subject: [PATCH 7/8] move random rng key if branch up --- crates/seismic/src/api/default_ctx.rs | 4 ++-- crates/seismic/src/chain/rng_container.rs | 12 ++-------- crates/seismic/src/chain/seismic_chain.rs | 28 ++++++++++++++++------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/seismic/src/api/default_ctx.rs b/crates/seismic/src/api/default_ctx.rs index 63357a1c..7b64eb56 100644 --- a/crates/seismic/src/api/default_ctx.rs +++ b/crates/seismic/src/api/default_ctx.rs @@ -28,14 +28,14 @@ impl DefaultSeismicContext for SeismicContext { Context::mainnet() .with_tx(SeismicTransaction::default()) .with_cfg(CfgEnv::new_with_spec(SeismicSpecId::MERCURY)) - .with_chain(SeismicChain::default()) + .with_chain(SeismicChain::with_random_rng_key()) } fn seismic_with_rng_key(rng_keypair: schnorrkel::Keypair) -> Self { Context::mainnet() .with_tx(SeismicTransaction::default()) .with_cfg(CfgEnv::new_with_spec(SeismicSpecId::MERCURY)) - .with_chain(SeismicChain::with_live_rng_key(Some(rng_keypair))) + .with_chain(SeismicChain::with_live_rng_key(rng_keypair)) } } diff --git a/crates/seismic/src/chain/rng_container.rs b/crates/seismic/src/chain/rng_container.rs index f0287c79..56c3744e 100644 --- a/crates/seismic/src/chain/rng_container.rs +++ b/crates/seismic/src/chain/rng_container.rs @@ -3,8 +3,6 @@ use revm::{ precompile::PrecompileError, primitives::{Bytes, B256}, }; -use schnorrkel::ExpansionMode; - /// Derives random bytes for the RNG precompile. /// /// Each call is fully stateless: a fresh `RootRng` is constructed from the @@ -19,16 +17,10 @@ pub fn derive_rng_output( pers: &[u8], requested_output_len: usize, tx_hash: &B256, - live_key: Option, + live_key: schnorrkel::Keypair, total_gas_remaining: u64, ) -> Result { - let key = live_key.unwrap_or_else(|| { - schnorrkel::MiniSecretKey::generate() - .expand(ExpansionMode::Uniform) - .into() - }); - - let mut rng = RootRng::new(key); + let mut rng = RootRng::new(live_key); rng.append_tx(tx_hash); rng.append_gas_left(total_gas_remaining); let rng_bytes = rng.derive_bytes(pers, requested_output_len); diff --git a/crates/seismic/src/chain/seismic_chain.rs b/crates/seismic/src/chain/seismic_chain.rs index 5b3f2f92..6ee267c9 100644 --- a/crates/seismic/src/chain/seismic_chain.rs +++ b/crates/seismic/src/chain/seismic_chain.rs @@ -2,12 +2,13 @@ use revm::{ precompile::PrecompileError, primitives::{Bytes, B256}, }; +use schnorrkel::ExpansionMode; use super::rng_container::derive_rng_output; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct SeismicChain { - live_rng_key: Option, + live_rng_key: schnorrkel::Keypair, /// Total remaining gas across all active call frames, set before precompile dispatch. gas_remaining_all_frames: u64, } @@ -15,12 +16,21 @@ pub struct SeismicChain { impl SeismicChain { pub fn new(root_vrf_key: schnorrkel::Keypair) -> Self { Self { - live_rng_key: Some(root_vrf_key), + live_rng_key: root_vrf_key, gas_remaining_all_frames: 0, } } - pub fn with_live_rng_key(live_rng_key: Option) -> Self { + pub fn with_random_rng_key() -> Self { + Self { + live_rng_key: schnorrkel::MiniSecretKey::generate() + .expand(ExpansionMode::Uniform) + .into(), + gas_remaining_all_frames: 0, + } + } + + pub fn with_live_rng_key(live_rng_key: schnorrkel::Keypair) -> Self { Self { live_rng_key, gas_remaining_all_frames: 0, @@ -28,7 +38,7 @@ impl SeismicChain { } pub fn set_rng_key(&mut self, root_vrf_key: schnorrkel::Keypair) { - self.live_rng_key = Some(root_vrf_key); + self.live_rng_key = root_vrf_key; } pub fn gas_remaining_all_frames(&self) -> u64 { @@ -137,12 +147,14 @@ mod tests { #[test] fn test_simulation_mode_non_deterministic() { // No live key = simulation mode (random key per call) - let chain = SeismicChain::default(); + let chain1 = SeismicChain::with_random_rng_key(); + let chain2 = SeismicChain::with_random_rng_key(); + let tx_hash = B256::from([1u8; 32]); let pers = b"test_pers"; - let output1 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); - let output2 = chain.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + let output1 = chain1.process_rng(pers, 32, &tx_hash, 1000).unwrap(); + let output2 = chain2.process_rng(pers, 32, &tx_hash, 1000).unwrap(); assert_ne!( output1, output2, From 0e30bc3fb4bd71dad8a24d6040e03a29b280a445 Mon Sep 17 00:00:00 2001 From: daltoncoder Date: Tue, 17 Feb 2026 13:18:35 -0500 Subject: [PATCH 8/8] update semantic test in github actions --- .github/workflows/seismic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/seismic.yml b/.github/workflows/seismic.yml index db4f999f..a235a3b3 100644 --- a/.github/workflows/seismic.yml +++ b/.github/workflows/seismic.yml @@ -100,7 +100,7 @@ jobs: TEMP_DIR=$(mktemp -d) # Temporarily checking out the test--new-storage-opcode-semantics branch that fixes storage opcode semantic tests. # TODO(samlaf): switch back to seismic (default) branch after we merge https://github.com/SeismicSystems/seismic-solidity/pull/130 - git clone -b test--new-storage-opcode-semantics https://github.com/SeismicSystems/seismic-solidity.git "$TEMP_DIR/seismic-solidity" + git clone -b update-precompile-semantic-test https://github.com/SeismicSystems/seismic-solidity.git "$TEMP_DIR/seismic-solidity" echo "SEISMIC_SOLIDITY_PATH=$TEMP_DIR/seismic-solidity" >> $GITHUB_ENV echo "seismic-solidity cloned to: $TEMP_DIR/seismic-solidity" - name: Install latest ssolc release