From 78d71e5d45f2cedeff60548b08ce5112b42b2896 Mon Sep 17 00:00:00 2001 From: Karan Dhareshwar Date: Thu, 23 Jul 2026 07:22:48 -0500 Subject: [PATCH 1/3] implement PREVRANDAO --- executor/evm/src/request.rs | 7 +++ executor/evm/tests/executor.rs | 45 ++++++++++++++++ .../components/contract_runtime/operations.rs | 53 ++++++++++++++++--- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/executor/evm/src/request.rs b/executor/evm/src/request.rs index f46b51d1b6..c04daf803d 100644 --- a/executor/evm/src/request.rs +++ b/executor/evm/src/request.rs @@ -88,6 +88,7 @@ pub struct BlockContext { /// /// Defaults to chainspec `[evm].base_fee * [evm].wei_per_mote`. pub base_fee: Option, + pub prevrandao: evm::Hash, } impl BlockContext { @@ -105,6 +106,7 @@ impl BlockContext { timestamp: revm::primitives::U256::from(self.timestamp), gas_limit: self.gas_limit.unwrap_or(config.block_gas_limit), basefee, + prevrandao: Some(tx::to_revm_hash(self.prevrandao)), ..Default::default() }) } @@ -129,6 +131,7 @@ mod tests { beneficiary: evm::Address::ZERO, gas_limit: None, base_fee: None, + prevrandao: evm::Hash::new([0x42; evm::HASH_LENGTH]), }; let block = context .to_revm_block(&config) @@ -136,5 +139,9 @@ mod tests { assert_eq!(u128::from(block.basefee), config.base_fee_wei()); assert_ne!(block.basefee, config.base_fee); + assert_eq!( + block.prevrandao, + Some(revm::primitives::B256::from([0x42; evm::HASH_LENGTH])) + ); } } diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index bb1a42fed3..35964a74f5 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -106,6 +106,7 @@ fn block() -> BlockContext { beneficiary: evm::Address::ZERO, gas_limit: None, base_fee: None, + prevrandao: evm::Hash::new([0x99; evm::HASH_LENGTH]), } } @@ -168,6 +169,24 @@ fn blockhash_contract_init_code() -> Vec { init_code_returning(runtime) } +fn prevrandao_contract_init_code() -> Vec { + let runtime = vec![ + // bytes32 value = prevrandao(); + opcode::DIFFICULTY, + // mstore(0, value); + opcode::PUSH1, + 0, + opcode::MSTORE, + // return abi.encode(value); + opcode::PUSH1, + 32, + opcode::PUSH1, + 0, + opcode::RETURN, + ]; + init_code_returning(runtime) +} + fn return_word_contract_init_code(value: u8) -> Vec { let runtime = vec![ opcode::PUSH1, @@ -1799,3 +1818,29 @@ fn checked_calls_enforce_transaction_validation() { Err(Error::Revm(_)) )); } + +#[test] +fn prevrandao_uses_block_context() { + let executor = executor(EvmSpec::Prague); + let from = evm::Address::new([1; 20]); + let (mut tracking_copy, _tempdir) = tracking_copy(); + let contract = execute_call( + &executor, + &mut tracking_copy, + from, + None, + prevrandao_contract_init_code(), + ) + .created_contract_address + .expect("deploy should return a contract address"); + + let outcome = execute_call( + &executor, + &mut tracking_copy, + from, + Some(contract), + Vec::new(), + ); + + assert_eq!(outcome.output.as_slice(), block().prevrandao.as_ref()); +} diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 9a607ea052..294a8e408b 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -42,7 +42,7 @@ use casper_types::{ bytesrepr::{self, Bytes, ToBytes, U32_SERIALIZED_LENGTH}, contracts::NamedKeys, evm::{ - Address as EvmAddress, HaltReason as EvmHaltReason, Receipt as EvmReceipt, + Address as EvmAddress, HaltReason as EvmHaltReason, Hash as EvmHash, Receipt as EvmReceipt, ReceiptStatus as EvmReceiptStatus, }, execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, @@ -81,6 +81,7 @@ fn evm_block_context( block_height: u64, block_time: BlockTime, proposer: &PublicKey, + prevrandao: EvmHash, ) -> EvmBlockContext { EvmBlockContext { number: block_height, @@ -88,6 +89,7 @@ fn evm_block_context( beneficiary: EvmAddress::from_block_proposer_public_key(proposer), gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(chainspec.evm_config.base_fee_wei()), + prevrandao, } } @@ -684,12 +686,14 @@ pub fn execute_finalized_block( } } + let prevrandao = + EvmHash::new(Digest::hash_pair(parent_seed, [executable_block.random_bit as u8]).value()); state_root_hash = write_eip4788_beacon_roots( &scratch_state, state_root_hash, chainspec, protocol_version, - evm_block_context(chainspec, block_height, block_time, &proposer), + evm_block_context(chainspec, block_height, block_time, &proposer, prevrandao), parent_hash, )?; @@ -1188,8 +1192,16 @@ pub fn execute_finalized_block( _ if is_evm => { let evm_transaction = evm_transaction.expect("EVM transaction should exist"); let base_fee_wei = chainspec.evm_config.base_fee_wei(); - let block_context = - evm_block_context(chainspec, block_height, block_time, &proposer); + let random_bit = executable_block.random_bit; + let prevrandao = + EvmHash::new(Digest::hash_pair(parent_seed, [random_bit as u8]).value()); + let block_context = evm_block_context( + chainspec, + block_height, + block_time, + &proposer, + prevrandao, + ); let request = EvmExecuteRequest { block: block_context, kind: EvmExecuteKind::Transaction(Box::new(evm_transaction.clone())), @@ -2169,6 +2181,7 @@ where beneficiary: EvmAddress::ZERO, gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(base_fee_wei), + prevrandao: EvmHash::new(block_header.accumulated_seed().value()), }; let kind = if evm_transaction.is_unsigned_call() { EvmExecuteKind::Call(EvmExecutorCallRequest { @@ -2345,7 +2358,7 @@ pub(crate) fn compute_execution_results_checksum<'a>( mod tests { use super::*; use casper_storage::{global_state::state, tracking_copy::TrackingCopyExt}; - use casper_types::{EvmConfig, DEFAULT_WEI_PER_MOTE}; + use casper_types::{EvmConfig, Timestamp, DEFAULT_WEI_PER_MOTE}; #[test] fn should_not_raise_evm_min_cost_above_converted_fee() { @@ -2388,7 +2401,8 @@ mod tests { state::lmdb::make_temporary_global_state([]); let scratch_state = global_state.create_scratch(); let block_time = BlockTime::new(2_000); - let block_context = evm_block_context(&chainspec, 1, block_time, &PublicKey::System); + let block_context = + evm_block_context(&chainspec, 1, block_time, &PublicKey::System, EvmHash::ZERO); let parent_hash = BlockHash::new(Digest::from_raw([0x44; 32])); let updated_state_root_hash = write_eip4788_beacon_roots( @@ -2410,4 +2424,31 @@ mod tests { assert_eq!(entry, Some((block_context.timestamp, parent_hash))); } + + #[test] + fn evm_prevrandao_matches_current_block_accumulated_seed() { + let parent_hash = BlockHash::new(Digest::from_raw([0x11; Digest::LENGTH])); + let parent_seed = Digest::from_raw([0x22; Digest::LENGTH]); + let state_root_hash = Digest::from_raw([0x33; Digest::LENGTH]); + let random_bit = true; + let block = BlockV2::new( + parent_hash, + parent_seed, + state_root_hash, + random_bit, + None, + Timestamp::zero(), + EraId::new(1), + 1, + ProtocolVersion::V2_0_0, + PublicKey::System, + BTreeMap::new(), + Default::default(), + 1, + None, + ); + + let prevrandao = EvmHash::new(Digest::hash_pair(parent_seed, [random_bit as u8]).value()); + assert_eq!(prevrandao.as_ref(), block.accumulated_seed().as_ref()); + } } From 971ae4e87efa32068a06193400a841a76fe24fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Mon, 24 Aug 2026 13:48:44 -0700 Subject: [PATCH 2/3] Use parent seed for EVM PREVRANDAO Expose the previous block's accumulated seed through opcode 0x44 during finalized and speculative execution. This prevents the current block's random bit from influencing PREVRANDAO and keeps both execution paths consistent. Update the executor test for the data access layer API and verify that both current-block random-bit values leave PREVRANDAO unchanged. --- executor/evm/tests/executor.rs | 4 +- .../components/contract_runtime/operations.rs | 47 ++++++++++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 6ce2fae103..c75cfffb7c 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -2176,9 +2176,10 @@ fn checked_calls_enforce_transaction_validation() { fn prevrandao_uses_block_context() { let executor = executor(EvmSpec::Prague); let from = evm::Address::new([1; 20]); - let (mut tracking_copy, _tempdir) = tracking_copy(); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); let contract = execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, None, @@ -2189,6 +2190,7 @@ fn prevrandao_uses_block_context() { let outcome = execute_call( &executor, + &data_access_layer, &mut tracking_copy, from, Some(contract), diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 01a7783283..0a862b67c6 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -81,6 +81,10 @@ fn evm_block_context( } } +fn evm_prevrandao(parent_seed: Digest) -> EvmHash { + EvmHash::new(parent_seed.value()) +} + fn write_eip4788_beacon_roots( scratch_state: &ScratchGlobalState, state_root_hash: Digest, @@ -673,8 +677,7 @@ pub fn execute_finalized_block( } } - let prevrandao = - EvmHash::new(Digest::hash_pair(parent_seed, [executable_block.random_bit as u8]).value()); + let prevrandao = evm_prevrandao(parent_seed); state_root_hash = write_eip4788_beacon_roots( &scratch_state, state_root_hash, @@ -1179,9 +1182,6 @@ pub fn execute_finalized_block( _ if is_evm => { let evm_transaction = evm_transaction.expect("EVM transaction should exist"); let base_fee_wei = chainspec.evm_config.base_fee_wei(); - let random_bit = executable_block.random_bit; - let prevrandao = - EvmHash::new(Digest::hash_pair(parent_seed, [random_bit as u8]).value()); let block_context = evm_block_context( chainspec, block_height, @@ -2158,7 +2158,7 @@ where beneficiary: EvmAddress::ZERO, gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(base_fee_wei), - prevrandao: EvmHash::new(block_header.accumulated_seed().value()), + prevrandao: evm_prevrandao(*block_header.accumulated_seed()), }; let kind = if evm_transaction.is_unsigned_call() { EvmExecuteKind::Call(EvmExecutorCallRequest { @@ -2402,16 +2402,31 @@ mod tests { } #[test] - fn evm_prevrandao_matches_current_block_accumulated_seed() { + fn evm_prevrandao_uses_parent_block_accumulated_seed() { let parent_hash = BlockHash::new(Digest::from_raw([0x11; Digest::LENGTH])); let parent_seed = Digest::from_raw([0x22; Digest::LENGTH]); let state_root_hash = Digest::from_raw([0x33; Digest::LENGTH]); - let random_bit = true; - let block = BlockV2::new( + let block_with_zero_bit = BlockV2::new( + parent_hash, + parent_seed, + state_root_hash, + false, + None, + Timestamp::zero(), + EraId::new(1), + 1, + ProtocolVersion::V2_0_0, + PublicKey::System, + BTreeMap::new(), + Default::default(), + 1, + None, + ); + let block_with_one_bit = BlockV2::new( parent_hash, parent_seed, state_root_hash, - random_bit, + true, None, Timestamp::zero(), EraId::new(1), @@ -2424,7 +2439,15 @@ mod tests { None, ); - let prevrandao = EvmHash::new(Digest::hash_pair(parent_seed, [random_bit as u8]).value()); - assert_eq!(prevrandao.as_ref(), block.accumulated_seed().as_ref()); + let prevrandao = evm_prevrandao(parent_seed); + assert_eq!(prevrandao.as_ref(), parent_seed.as_ref()); + assert_ne!( + prevrandao.as_ref(), + block_with_zero_bit.accumulated_seed().as_ref() + ); + assert_ne!( + prevrandao.as_ref(), + block_with_one_bit.accumulated_seed().as_ref() + ); } } From d0b89b1c1893c17adddaee585a975ec142164208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Mon, 24 Aug 2026 14:53:10 -0700 Subject: [PATCH 3/3] Resolve historical PREVRANDAO from parent blocks Use the parent block's accumulated seed for unsigned calls executed against a selected block. Continue using the selected block's seed when speculatively executing the following block. Return an invalid transaction when the required parent header cannot be loaded, and cover both execution modes in tests. --- .../components/contract_runtime/operations.rs | 94 +++++++++++++++++-- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index a2e3739ddd..42be4c9c81 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -15,7 +15,7 @@ use casper_executor_evm::{ ExecuteRequest as EvmExecuteRequest, ExecutionStatus as EvmExecutionStatus, }; use casper_storage::{ - block_store::types::ApprovalsHashes, + block_store::{lmdb::LmdbBlockStore, types::ApprovalsHashes}, data_access_layer::{ balance::BalanceHandling, mint::{BalanceIdentifierTransferArgs, BurnRequest}, @@ -85,6 +85,31 @@ fn evm_prevrandao(parent_seed: Digest) -> EvmHash { EvmHash::new(parent_seed.value()) } +fn speculative_evm_prevrandao( + block_store: &LmdbBlockStore, + block_header: &BlockHeader, + is_unsigned_call: bool, +) -> Result { + if !is_unsigned_call { + return Ok(evm_prevrandao(*block_header.accumulated_seed())); + } + if block_header.height() == 0 { + // The genesis block has no parent, so there is no PREVRANDAO value to return. + return Ok(EvmHash::ZERO); + } + + let parent_hash = *block_header.parent_hash(); + let transaction = block_store + .checkout_ro() + .map_err(|error| format!("failed to open block store for PREVRANDAO: {error}"))?; + let parent_header = transaction + .read_block_header_by_hash(parent_hash) + .map_err(|error| format!("failed to read PREVRANDAO parent {parent_hash}: {error}"))? + .ok_or_else(|| format!("PREVRANDAO parent block {parent_hash} not found"))?; + + Ok(evm_prevrandao(*parent_header.accumulated_seed())) +} + fn write_eip4788_beacon_roots( scratch_state: &ScratchGlobalState, state_root_hash: Digest, @@ -2119,6 +2144,7 @@ fn speculative_evm_block_context( chainspec: &Chainspec, block_header: &BlockHeader, is_unsigned_call: bool, + prevrandao: EvmHash, ) -> EvmBlockContext { if is_unsigned_call { let beneficiary = match block_header { @@ -2133,7 +2159,7 @@ fn speculative_evm_block_context( beneficiary, gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(chainspec.evm_config.base_fee_wei()), - prevrandao: evm_prevrandao(*block_header.accumulated_seed()), + prevrandao, } } else { let block_time = block_header @@ -2145,7 +2171,7 @@ fn speculative_evm_block_context( beneficiary: EvmAddress::ZERO, gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(chainspec.evm_config.base_fee_wei()), - prevrandao: evm_prevrandao(*block_header.accumulated_seed()), + prevrandao, } } } @@ -2193,7 +2219,20 @@ where }; let base_fee_wei = chainspec.evm_config.base_fee_wei(); let is_unsigned_call = evm_transaction.is_unsigned_call(); - let block_context = speculative_evm_block_context(chainspec, &block_header, is_unsigned_call); + let prevrandao = match speculative_evm_prevrandao( + &data_access_layer.block_store, + &block_header, + is_unsigned_call, + ) { + Ok(prevrandao) => prevrandao, + Err(error) => { + return SpeculativeExecutionResult::invalid_transaction(InvalidTransaction::Evm( + casper_types::EvmTransactionError::Decode(error), + )) + } + }; + let block_context = + speculative_evm_block_context(chainspec, &block_header, is_unsigned_call, prevrandao); let kind = if is_unsigned_call { EvmExecuteKind::Call(EvmExecutorCallRequest { from: evm_transaction.from(), @@ -2367,7 +2406,9 @@ pub(crate) fn compute_execution_results_checksum<'a>( #[cfg(test)] mod tests { use super::*; - use casper_storage::{global_state::state, tracking_copy::TrackingCopyExt}; + use casper_storage::{ + block_store::BlockStoreTransaction, global_state::state, tracking_copy::TrackingCopyExt, + }; use casper_types::{BlockHeaderV2, EvmConfig, Timestamp, DEFAULT_WEI_PER_MOTE}; #[test] @@ -2433,13 +2474,31 @@ mod tests { }; let timestamp = Timestamp::from(123_456_789); let proposer = PublicKey::System; - let block_header = BlockHeader::V2(BlockHeaderV2::new( + let parent_seed = Digest::from_raw([0x44; Digest::LENGTH]); + let parent_header = BlockHeader::V2(BlockHeaderV2::new( Default::default(), Default::default(), Default::default(), + false, + parent_seed, + Default::default(), + Timestamp::from(123_455_789), + Default::default(), + 41, + chainspec.protocol_version(), + proposer.clone(), + Default::default(), + Default::default(), Default::default(), + )); + let selected_seed = Digest::hash_pair(parent_seed, [1]); + let block_header = BlockHeader::V2(BlockHeaderV2::new( + parent_header.block_hash(), Default::default(), Default::default(), + true, + selected_seed, + Default::default(), timestamp, Default::default(), 42, @@ -2449,8 +2508,19 @@ mod tests { Default::default(), Default::default(), )); - - let context = speculative_evm_block_context(&chainspec, &block_header, true); + let mut block_store = + LmdbBlockStore::new_temporary(64 * 1024 * 1024).expect("should create block store"); + let mut transaction = block_store + .checkout_rw() + .expect("should check out write transaction"); + transaction + .write_block_header(&parent_header) + .expect("should write parent header"); + transaction.commit().expect("should commit parent header"); + + let prevrandao = speculative_evm_prevrandao(&block_store, &block_header, true) + .expect("should resolve parent seed"); + let context = speculative_evm_block_context(&chainspec, &block_header, true, prevrandao); assert_eq!(context.number, 42); assert_eq!(context.timestamp, timestamp.millis() / 1_000); @@ -2458,6 +2528,14 @@ mod tests { context.beneficiary, EvmAddress::from_block_proposer_public_key(&proposer) ); + assert_eq!(context.prevrandao, evm_prevrandao(parent_seed)); + assert_ne!(context.prevrandao, evm_prevrandao(selected_seed)); + + assert_eq!( + speculative_evm_prevrandao(&block_store, &block_header, false) + .expect("should use selected seed for next-block execution"), + evm_prevrandao(selected_seed) + ); } #[test]