From 7805c8a8cac1d2cbed39442d7cc6f7ac421dc83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Wed, 23 Sep 2026 17:15:24 +0200 Subject: [PATCH] Reject missing referenced EVM bytecode Return a typed database error when a non-empty code hash has no bytecode record so corrupted state cannot execute as empty code. Preserve canonical empty-code accounts and existing type errors. Add regression coverage for transfers, nested execution, delegation, system calls, and distinct bytecode integrity failures. --- executor/evm/src/db.rs | 91 ++++++++++++++- executor/evm/src/error.rs | 6 + executor/evm/tests/executor.rs | 203 +++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 1 deletion(-) diff --git a/executor/evm/src/db.rs b/executor/evm/src/db.rs index fa8d0ef3e3..ffca50fa5c 100644 --- a/executor/evm/src/db.rs +++ b/executor/evm/src/db.rs @@ -134,7 +134,9 @@ where expected: "StoredValue::ByteCode", found: stored_value.type_name(), }), - None => Ok(Bytecode::default()), + // Empty-code accounts do not require a stored bytecode record. + None if code_hash == evm::EMPTY_CODE_HASH => Ok(Bytecode::default()), + None => Err(DbError::MissingBytecode { key: Box::new(key) }), } } @@ -186,3 +188,90 @@ fn cl_value_to_u256(key: Key, cl_value: CLValue, wei_per_mote: u64) -> Result, + ) -> Result { + let key = Key::Evm(EvmAddr::ByteCode(code_hash)); + let (state, root, _tempdir) = + make_temporary_global_state(stored_value.map(|value| (key, value))); + let reader = state.checkout(root).unwrap().unwrap(); + let mut tracking_copy = TrackingCopy::new(reader, 5, false); + let data_access_layer = DataAccessLayer { + block_store: LmdbBlockStore::new_temporary(64 * 1024 * 1024).unwrap(), + state, + max_query_depth: 5, + enable_addressable_entity: false, + }; + CasperDb::new(&data_access_layer, &mut tracking_copy, 1) + .code_by_hash(tx::to_revm_hash(code_hash)) + } + + #[test] + fn empty_code_hash_does_not_require_stored_bytecode() { + assert!(read_bytecode(evm::EMPTY_CODE_HASH, None) + .unwrap() + .is_empty()); + } + + #[test] + fn non_empty_code_hash_requires_stored_bytecode() { + // The all-zero hash is not the canonical empty-code hash. + for hash in [evm::Hash::new([0x42; 32]), evm::Hash::new([0; 32])] { + let error = read_bytecode(hash, None).unwrap_err(); + assert!(matches!( + error, + DbError::MissingBytecode { key } if *key == Key::Evm(EvmAddr::ByteCode(hash)) + )); + } + } + + #[test] + fn invalid_bytecode_records_preserve_type_errors() { + let wrong_value = StoredValue::CLValue(CLValue::from_t(1u64).unwrap()); + let mut cases = vec![( + wrong_value.clone(), + "StoredValue::ByteCode", + wrong_value.type_name(), + )]; + for kind in [ + ByteCodeKind::Empty, + ByteCodeKind::V1CasperWasm, + ByteCodeKind::V2CasperWasm, + ] { + cases.push(( + StoredValue::ByteCode(ByteCode::new(kind, vec![])), + "EVM bytecode kind", + kind.to_string(), + )); + } + + for (value, expected_type, found_type) in cases { + let hash = evm::Hash::new([0x42; 32]); + let error = read_bytecode(hash, Some(value)).unwrap_err(); + match error { + DbError::TypeMismatch { + key, + expected, + found, + } => { + assert_eq!(*key, Key::Evm(EvmAddr::ByteCode(hash))); + assert_eq!(expected, expected_type); + assert_eq!(found, found_type); + } + other => panic!("expected bytecode type mismatch, got {other:?}"), + } + } + } +} diff --git a/executor/evm/src/error.rs b/executor/evm/src/error.rs index 06cc564ce7..0f9593b344 100644 --- a/executor/evm/src/error.rs +++ b/executor/evm/src/error.rs @@ -48,6 +48,12 @@ pub enum DbError { /// Failed while reading from the tracking copy. #[error(transparent)] TrackingCopy(#[from] TrackingCopyError), + /// The bytecode referenced by a non-empty EVM code hash is missing. + #[error("missing referenced EVM bytecode at {key}")] + MissingBytecode { + /// Global-state key of the missing bytecode. + key: Box, + }, /// The value stored under an EVM key has an unexpected variant. #[error("unexpected stored value for {key}: expected {expected}, found {found}")] TypeMismatch { diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 5de6054738..60fee5525c 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -960,6 +960,209 @@ fn delegation_code(delegate: evm::Address) -> Vec { code } +#[test] +fn missing_bytecode_rejects_signed_value_transfer_without_state_changes() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let target = evm::Address::new([0xa1; 20]); + let transaction = legacy_transaction_to( + Some(7), + to_alloy_address(target), + U256::from(DEFAULT_WEI_PER_MOTE), + 100_000, + ); + let sender = transaction.from(); + seed_evm_balance(&mut tracking_copy, sender, U512::from(100)); + seed_evm_balance(&mut tracking_copy, target, U512::zero()); + seed_evm_code(&mut tracking_copy, target, reverting_runtime()); + let missing_key = Key::Evm(EvmAddr::ByteCode(read_code_hash( + &mut tracking_copy, + target, + ))); + tracking_copy.prune(missing_key); + + let error = executor + .execute( + &data_access_layer, + &mut tracking_copy, + ExecuteRequest { + block: block(), + kind: ExecuteKind::Transaction(Box::new(transaction)), + }, + ) + .expect_err("missing contract bytecode must abort execution"); + + assert!(matches!( + error, + Error::Database(DbError::MissingBytecode { key }) if *key == missing_key + )); + assert_eq!(read_balance(&mut tracking_copy, sender), U512::from(100)); + assert_eq!(read_balance(&mut tracking_copy, target), U512::zero()); + assert_eq!(read_evm_nonce(&mut tracking_copy, sender), 0); +} + +#[test] +fn empty_accounts_accept_value_transfers_without_stored_bytecode() { + for has_code_hash in [true, false] { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let target = evm::Address::new([0xa1; 20]); + let transaction = legacy_transaction_to( + Some(7), + to_alloy_address(target), + U256::from(DEFAULT_WEI_PER_MOTE), + 21_000, + ); + let sender = transaction.from(); + seed_evm_balance(&mut tracking_copy, sender, U512::from(100)); + seed_evm_balance(&mut tracking_copy, target, U512::zero()); + if !has_code_hash { + tracking_copy.prune(Key::Evm(EvmAddr::CodeHash(target))); + } + assert!(read_code(&mut tracking_copy, EMPTY_CODE_HASH).is_none()); + + let outcome = execute_transaction( + &executor, + &data_access_layer, + &mut tracking_copy, + transaction, + ); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!(read_balance(&mut tracking_copy, sender), U512::from(99)); + assert_eq!(read_balance(&mut tracking_copy, target), U512::one()); + assert_eq!(read_evm_nonce(&mut tracking_copy, sender), 1); + assert_eq!(read_code_hash(&mut tracking_copy, target), EMPTY_CODE_HASH); + assert!(read_code(&mut tracking_copy, EMPTY_CODE_HASH).is_none()); + } +} + +#[test] +fn missing_bytecode_aborts_nested_execution_without_state_changes() { + for instruction in [opcode::CALL, opcode::EXTCODESIZE, opcode::EXTCODECOPY] { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let sender = evm::Address::new([0xa0; 20]); + let outer = evm::Address::new([0xa1; 20]); + let inner = evm::Address::new([0xa2; 20]); + seed_evm_balance(&mut tracking_copy, sender, U512::from(100)); + seed_evm_balance(&mut tracking_copy, outer, U512::zero()); + seed_evm_code(&mut tracking_copy, inner, reverting_runtime()); + let missing_key = Key::Evm(EvmAddr::ByteCode(read_code_hash(&mut tracking_copy, inner))); + tracking_copy.prune(missing_key); + + // Write storage before loading the corrupt account to verify that the + // database error discards earlier execution as well as the value transfer. + let mut runtime = vec![opcode::PUSH1, 1, opcode::PUSH0, opcode::SSTORE]; + match instruction { + opcode::CALL => runtime.extend_from_slice(&[ + opcode::PUSH0, // return size + opcode::PUSH0, // return offset + opcode::PUSH0, // input size + opcode::PUSH0, // input offset + opcode::PUSH0, // value + ]), + opcode::EXTCODECOPY => runtime.extend_from_slice(&[ + opcode::PUSH1, + 32, // size + opcode::PUSH0, // code offset + opcode::PUSH0, // memory offset + ]), + _ => {} + } + runtime.push(opcode::PUSH20); + runtime.extend_from_slice(inner.as_bytes()); + if instruction == opcode::CALL { + runtime.push(opcode::GAS); + } + runtime.extend_from_slice(&[instruction, opcode::STOP]); + seed_evm_code(&mut tracking_copy, outer, runtime); + + let error = executor + .execute( + &data_access_layer, + &mut tracking_copy, + checked_call_request(sender, Some(outer), Vec::new(), CasperU256::one()), + ) + .expect_err("missing nested bytecode must abort execution"); + + assert!(matches!( + error, + Error::Database(DbError::MissingBytecode { key }) if *key == missing_key + )); + assert_eq!( + read_storage(&mut tracking_copy, outer, CasperU256::zero()), + None + ); + assert_eq!(read_balance(&mut tracking_copy, sender), U512::from(100)); + assert_eq!(read_balance(&mut tracking_copy, outer), U512::zero()); + assert_eq!(read_evm_nonce(&mut tracking_copy, sender), 0); + } +} + +#[test] +fn missing_bytecode_rejects_delegated_call() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let authority = evm::Address::new([0xa1; 20]); + let delegate = evm::Address::new([0xa2; 20]); + seed_evm_code(&mut tracking_copy, authority, delegation_code(delegate)); + seed_evm_code(&mut tracking_copy, delegate, reverting_runtime()); + let missing_key = Key::Evm(EvmAddr::ByteCode(read_code_hash( + &mut tracking_copy, + delegate, + ))); + tracking_copy.prune(missing_key); + + let error = executor + .execute( + &data_access_layer, + &mut tracking_copy, + call_request( + evm::Address::ZERO, + Some(authority), + Vec::new(), + CasperU256::zero(), + ), + ) + .expect_err("missing delegated bytecode must abort execution"); + + assert!(matches!( + error, + Error::Database(DbError::MissingBytecode { key }) if *key == missing_key + )); +} + +#[test] +fn missing_bytecode_rejects_system_call() { + let executor = executor(EvmSpec::Prague); + let (mut tracking_copy, data_access_layer, _tempdir) = tracking_copy(); + let target = evm::Address::new([0xa1; 20]); + seed_evm_code(&mut tracking_copy, target, reverting_runtime()); + let missing_key = Key::Evm(EvmAddr::ByteCode(read_code_hash( + &mut tracking_copy, + target, + ))); + tracking_copy.prune(missing_key); + + let error = executor + .execute_system_call( + &data_access_layer, + &mut tracking_copy, + SystemCallRequest { + block: block(), + target, + input: Vec::new(), + }, + ) + .expect_err("missing system-call bytecode must abort execution"); + + assert!(matches!( + error, + Error::Database(DbError::MissingBytecode { key }) if *key == missing_key + )); +} + #[test] fn prague_bls12_g1_add_precompile_delegates_to_revm() { let executor = executor(EvmSpec::Prague);