From 624edb471704c83a17743e7138582767712b403c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Papierski?= Date: Tue, 14 Jul 2026 14:38:49 +0200 Subject: [PATCH] Add InitiatorAddr::Eoa --- EVM.md | 8 +- binary_port/src/error_code.rs | 14 ++ execution_engine/src/engine_state/mod.rs | 14 +- .../src/transfer_request_builder.rs | 8 +- .../test_support/src/wasm_test_builder.rs | 6 +- .../components/contract_runtime/operations.rs | 156 +++++++----------- .../operations/wasm_v2_request.rs | 8 +- node/src/components/contract_runtime/types.rs | 6 +- node/src/components/transaction_acceptor.rs | 10 +- .../main_reactor/tests/transactions.rs | 6 +- .../src/types/transaction/meta_transaction.rs | 17 +- .../transaction/meta_transaction/meta_evm.rs | 2 +- .../meta_transaction/meta_transaction_v1.rs | 35 +++- .../meta_transaction/transaction_header.rs | 2 +- resources/test/sse_data_schema.json | 101 ++++++------ storage/src/data_access_layer.rs | 4 +- storage/src/data_access_layer/balance.rs | 33 +++- storage/src/global_state/state/mod.rs | 26 ++- types/src/evm/address.rs | 63 ++++++- types/src/evm/transaction.rs | 63 ++----- types/src/gens.rs | 2 + types/src/transaction.rs | 2 +- types/src/transaction/deploy.rs | 2 +- types/src/transaction/initiator_addr.rs | 110 +++++++++++- types/src/transaction/transaction_v1.rs | 5 + .../transaction/transaction_v1/errors_v1.rs | 10 ++ types/tests/evm_transaction.rs | 20 ++- 27 files changed, 477 insertions(+), 256 deletions(-) diff --git a/EVM.md b/EVM.md index 1305414aeb..126bbed393 100644 --- a/EVM.md +++ b/EVM.md @@ -206,6 +206,12 @@ a raw signed RLP blob. The EVM transaction is stored as: - Ethereum signed transaction hash: `hash`. - Exactly one Casper `Approval` containing the Ethereum secp256k1 signature. +The generic transaction initiator is not stored as another EVM transaction +field. `EvmTransaction::initiator_addr()` derives +`InitiatorAddr::Eoa(transaction.from())` on demand. This keeps the 20-byte EVM +identity intact and avoids requiring callers such as sidecar's `eth_call` path +to fabricate a Casper `AccountHash` for an Ethereum address. + `evm::Hash`, `evm::Topic`, and `evm::TransactionHash` are `Digest`-backed wrappers, but their constructors preserve the supplied 32 bytes as raw Ethereum values. They do not hash the bytes again. `evm::Hash` is used for @@ -374,7 +380,7 @@ meta_transaction = MetaTransaction::from_transaction(stored_transaction, ...) Common metadata such as hash, authorization keys, size estimate, gas limit, and cost is derived directly from `Transaction`. For EVM: -- the initiator is the EVM sender address, `transaction.from()`, +- the initiator is `InitiatorAddr::Eoa(transaction.from())`, derived on demand, - the transaction lane is currently the last configured Wasm lane, - the gas limit is the Ethereum transaction gas limit, - the maximum cost is diff --git a/binary_port/src/error_code.rs b/binary_port/src/error_code.rs index 4f9ddb35a3..03a8c200fd 100644 --- a/binary_port/src/error_code.rs +++ b/binary_port/src/error_code.rs @@ -376,6 +376,9 @@ pub enum ErrorCode { /// EVM transaction nonce does not match the account nonce. #[error("the EVM transaction nonce does not match the account nonce")] InvalidTransactionEvmInvalidNonce = 119, + /// EOA initiators are not valid for V1 transactions. + #[error("invalid initiator address for Transaction::V1")] + InvalidTransactionInvalidInitiatorAddr = 120, } impl TryFrom for ErrorCode { @@ -481,6 +484,9 @@ impl From for ErrorCode { } InvalidTransactionV1::InvalidBodyHash => ErrorCode::InvalidTransactionBodyHash, InvalidTransactionV1::InvalidTransactionHash => ErrorCode::InvalidTransactionHash, + InvalidTransactionV1::InvalidInitiatorAddr => { + ErrorCode::InvalidTransactionInvalidInitiatorAddr + } InvalidTransactionV1::EmptyApprovals => ErrorCode::InvalidTransactionEmptyApprovals, InvalidTransactionV1::InvalidApproval { .. } => { ErrorCode::InvalidTransactionInvalidApproval @@ -642,6 +648,14 @@ mod tests { ); } + #[test] + fn invalid_v1_eoa_initiator_has_specific_error_code() { + let code = ErrorCode::from(InvalidTransactionV1::InvalidInitiatorAddr); + + assert_eq!(code, ErrorCode::InvalidTransactionInvalidInitiatorAddr); + assert_eq!(code as u16, 120); + } + #[test] fn try_from_decoded_all_variants() { for variant in ErrorCode::iter() { diff --git a/execution_engine/src/engine_state/mod.rs b/execution_engine/src/engine_state/mod.rs index b0b201afcb..fe7b6c4a00 100644 --- a/execution_engine/src/engine_state/mod.rs +++ b/execution_engine/src/engine_state/mod.rs @@ -76,7 +76,12 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr.account_hash(); + let Some(account_hash) = initiator_addr.account_hash() else { + return WasmV1Result::precondition_failure( + gas_limit, + Error::TrackingCopy(TrackingCopyError::Authorization), + ); + }; let protocol_version = self.config.protocol_version(); let state_hash = block_info.state_hash; let tc = match state_provider.tracking_copy(state_hash) { @@ -155,7 +160,12 @@ impl ExecutionEngineV1 { // A good deal of effort has been put into removing all such behaviors; please do not // come along and start adding it back. - let account_hash = initiator_addr.account_hash(); + let Some(account_hash) = initiator_addr.account_hash() else { + return WasmV1Result::precondition_failure( + gas_limit, + Error::TrackingCopy(TrackingCopyError::Authorization), + ); + }; let protocol_version = self.config.protocol_version(); let tc = Rc::new(RefCell::new(tracking_copy)); let (runtime_footprint, entity_addr) = { diff --git a/execution_engine_testing/test_support/src/transfer_request_builder.rs b/execution_engine_testing/test_support/src/transfer_request_builder.rs index 46da00f8f0..1f077172ea 100644 --- a/execution_engine_testing/test_support/src/transfer_request_builder.rs +++ b/execution_engine_testing/test_support/src/transfer_request_builder.rs @@ -128,9 +128,11 @@ impl TransferRequestBuilder { /// authorization keys. pub fn with_initiator>(mut self, initiator: T) -> Self { self.initiator = initiator.into(); - let _ = self - .authorization_keys - .insert(self.initiator.account_hash()); + let _ = self.authorization_keys.insert( + self.initiator + .account_hash() + .expect("test transfer initiator must be a Casper account"), + ); self } diff --git a/execution_engine_testing/test_support/src/wasm_test_builder.rs b/execution_engine_testing/test_support/src/wasm_test_builder.rs index 70b8db9115..97c8dda244 100644 --- a/execution_engine_testing/test_support/src/wasm_test_builder.rs +++ b/execution_engine_testing/test_support/src/wasm_test_builder.rs @@ -813,7 +813,11 @@ where .expect("builder must have a post-state hash"); let transaction_hash = TransactionHash::V1(TransactionV1Hash::default()); - let authorization_keys = BTreeSet::from_iter(iter::once(initiator.account_hash())); + let authorization_keys = BTreeSet::from_iter(iter::once( + initiator + .account_hash() + .expect("test bidding initiator must be a Casper account"), + )); let config = &self.chainspec; let fee_handling = config.core_config.fee_handling; diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index abef7d36b6..f98523b655 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -49,7 +49,7 @@ use casper_types::{ execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, system::handle_payment::ARG_AMOUNT, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, - EntityAddr, EraEndV2, EraId, EvmSpec, FeeHandling, Gas, InitiatorAddr, InvalidTransaction, + EntityAddr, EraEndV2, EraId, EvmSpec, FeeHandling, Gas, InvalidTransaction, InvalidTransactionV1, Key, ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, Transaction, TransactionEntryPoint, AUCTION_LANE_ID, MINT_LANE_ID, U512, }; @@ -164,82 +164,25 @@ fn execution_min_cost( } #[derive(Clone, Debug)] -enum RuntimeOrigin { - Initiator { - initiator_addr: InitiatorAddr, - payer: BalanceIdentifier, - }, - Evm { - // Concrete payer selected before payment checks. This is deliberately a - // data-access balance identifier, not an EVM-specific balance mode, so - // the rest of block execution can use the normal hold/refund/fee - // machinery. - balance_identifier: BalanceIdentifier, - // State mutation to perform later, inside the same tracking copy as EVM - // execution. Origin resolution itself is read-only so a rejected - // transaction does not create accounts or links as a side effect. - identity_plan: EvmIdentityPlan, - }, +struct EvmOriginResolution { + // Concrete payer selected before payment checks. This is deliberately a + // data-access balance identifier, not an EVM-specific balance mode, so + // the rest of block execution can use the normal hold/refund/fee + // machinery. + balance_identifier: BalanceIdentifier, + // State mutation to perform later, inside the same tracking copy as EVM + // execution. Origin resolution itself is read-only so a rejected + // transaction does not create accounts or links as a side effect. + identity_plan: EvmIdentityPlan, } -impl RuntimeOrigin { - fn from_initiator_addr(initiator_addr: InitiatorAddr) -> Self { - RuntimeOrigin::Initiator { - payer: BalanceIdentifier::from(initiator_addr.clone()), - initiator_addr, - } - } - - fn from_evm_parts( - balance_identifier: BalanceIdentifier, - identity_plan: EvmIdentityPlan, - ) -> Self { - RuntimeOrigin::Evm { +impl EvmOriginResolution { + fn new(balance_identifier: BalanceIdentifier, identity_plan: EvmIdentityPlan) -> Self { + Self { balance_identifier, identity_plan, } } - - fn payer_balance_identifier(&self) -> BalanceIdentifier { - match self { - RuntimeOrigin::Initiator { payer, .. } => payer.clone(), - RuntimeOrigin::Evm { - balance_identifier, .. - } => balance_identifier.clone(), - } - } - - fn initiator_addr(&self) -> Result { - match self { - RuntimeOrigin::Initiator { initiator_addr, .. } => Ok(initiator_addr.clone()), - RuntimeOrigin::Evm { .. } => Err(BlockExecutionError::InvalidTransactionVariant), - } - } - - fn account_hash(&self) -> Result { - self.initiator_addr() - .map(|initiator_addr| initiator_addr.account_hash()) - } - - fn fee_initiator(&self) -> Option> { - match self { - RuntimeOrigin::Initiator { initiator_addr, .. } => { - Some(Box::new(initiator_addr.clone())) - } - RuntimeOrigin::Evm { .. } => None, - } - } - - fn evm_identity_plan(&self) -> Option { - match self { - RuntimeOrigin::Initiator { .. } => None, - RuntimeOrigin::Evm { identity_plan, .. } => Some(*identity_plan), - } - } - - fn is_evm(&self) -> bool { - matches!(self, RuntimeOrigin::Evm { .. }) - } } /// Deferred write needed to make an EVM sender's identity explicit in global state. @@ -273,12 +216,12 @@ enum EvmIdentityPlan { /// This function only reads state. That matters because it runs before payment /// preconditions are known to pass. If execution is later allowed, the returned /// [`EvmIdentityPlan`] is applied in the tracking copy used for EVM execution. -fn resolve_evm_runtime_origin( +fn resolve_evm_origin( scratch_state: &ScratchGlobalState, state_root_hash: Digest, protocol_version: ProtocolVersion, transaction: &casper_types::EvmTransaction, -) -> Result { +) -> Result { let address = transaction.from(); // The signer gives us a Casper `AccountHash` preimage from the secp256k1 // public key. That account hash is not derivable from the 20-byte EVM @@ -313,7 +256,7 @@ fn resolve_evm_runtime_origin( // Existing bridge records are authoritative. Once an EVM // address is linked, the payer is the linked Casper account's // main purse. - Key::Account(account_hash) => Ok(RuntimeOrigin::from_evm_parts( + Key::Account(account_hash) => Ok(EvmOriginResolution::new( BalanceIdentifier::Account(account_hash), EvmIdentityPlan::None, )), @@ -330,7 +273,7 @@ fn resolve_evm_runtime_origin( purse, deterministic_purse, )?; - Ok(RuntimeOrigin::from_evm_parts( + Ok(EvmOriginResolution::new( BalanceIdentifier::Purse(purse), identity_plan, )) @@ -349,7 +292,7 @@ fn resolve_evm_runtime_origin( // already a contract/runtime-created EVM account. Contracts do not // have a signing key, so they must remain EVM-native. if evm_account_has_code(&mut tracking_copy, address)? { - return Ok(RuntimeOrigin::from_evm_parts( + return Ok(EvmOriginResolution::new( BalanceIdentifier::Purse(deterministic_purse), EvmIdentityPlan::None, )); @@ -358,7 +301,7 @@ fn resolve_evm_runtime_origin( // A Casper account exists for the recovered signer, but the EVM // address has not been seen before. Use the account for payment // immediately and write the bridge only if execution proceeds. - Some(_) => Ok(RuntimeOrigin::from_evm_parts( + Some(_) => Ok(EvmOriginResolution::new( BalanceIdentifier::Account(account_hash), EvmIdentityPlan::LinkExisting { address, @@ -368,7 +311,7 @@ fn resolve_evm_runtime_origin( // First use of this signing pair on both sides. Runtime will // create a Casper account whose main purse is the deterministic // EVM purse, then write the bridge record. - None => Ok(RuntimeOrigin::from_evm_parts( + None => Ok(EvmOriginResolution::new( BalanceIdentifier::Purse(deterministic_purse), EvmIdentityPlan::CreateAccount { address, @@ -874,18 +817,25 @@ pub fn execute_finalized_block( let is_custom_payment = !is_standard_payment && transaction.is_custom_payment(); let is_v1_wasm = transaction.is_v1_wasm(); let is_v2_wasm = transaction.is_v2_wasm(); - let runtime_origin = if let Some(evm_transaction) = evm_transaction { - resolve_evm_runtime_origin( + let initiator_addr = stored_transaction.initiator_addr(); + let evm_origin_resolution = if let Some(evm_transaction) = evm_transaction { + Some(resolve_evm_origin( &scratch_state, state_root_hash, protocol_version, evm_transaction, - )? + )?) } else { - let initiator_addr = stored_transaction.initiator_addr(); - RuntimeOrigin::from_initiator_addr(initiator_addr) + None + }; + let payer_balance_identifier = if let Some(resolution) = &evm_origin_resolution { + resolution.balance_identifier.clone() + } else { + initiator_addr + .clone() + .try_into() + .map_err(|_| BlockExecutionError::InvalidTransactionVariant)? }; - let payer_balance_identifier = runtime_origin.payer_balance_identifier(); let refund_purse_active = is_custom_payment; if refund_purse_active { @@ -952,7 +902,7 @@ pub fn execute_finalized_block( } let mut balance_identifier = { - if runtime_origin.is_evm() { + if is_evm { // EVM transactions intentionally do not participate in Casper custom payment // or refund-purse setup. Ethereum payloads carry a gas limit and gas price fields, // but this chain still owns the fee/refund policy through the same chainspec @@ -979,7 +929,11 @@ pub fn execute_finalized_block( artifact_builder .with_state_result_error(err) .map_err(|_| BlockExecutionError::RootNotFound(state_root_hash))?; - BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) + BalanceIdentifier::PenalizedAccount( + initiator_addr + .account_hash() + .ok_or(BlockExecutionError::InvalidTransactionVariant)?, + ) } } } else { @@ -1035,7 +989,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - runtime_origin.initiator_addr()?, + initiator_addr.clone(), authorization_keys.clone(), BalanceIdentifierTransferArgs::new( None, @@ -1080,7 +1034,11 @@ pub fn execute_finalized_block( BalanceIdentifier::Payment } } else { - BalanceIdentifier::PenalizedAccount(runtime_origin.account_hash()?) + BalanceIdentifier::PenalizedAccount( + initiator_addr + .account_hash() + .ok_or(BlockExecutionError::InvalidTransactionVariant)?, + ) } }; @@ -1172,7 +1130,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - runtime_origin.initiator_addr()?, + initiator_addr.clone(), authorization_keys, runtime_args.clone(), )); @@ -1188,7 +1146,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - runtime_origin.initiator_addr()?, + initiator_addr.clone(), authorization_keys, runtime_args.clone(), )); @@ -1218,7 +1176,7 @@ pub fn execute_finalized_block( state_root_hash, protocol_version, transaction_hash, - runtime_origin.initiator_addr()?, + initiator_addr.clone(), authorization_keys, auction_method, )); @@ -1253,7 +1211,7 @@ pub fn execute_finalized_block( let mut tracking_copy = scratch_state .tracking_copy(state_root_hash)? .ok_or(BlockExecutionError::RootNotFound(state_root_hash))?; - if let Some(identity_plan) = runtime_origin.evm_identity_plan() { + if let Some(resolution) = &evm_origin_resolution { // Apply the deferred bridge/account creation only now, // after balance preconditions have allowed execution. // This keeps rejected EVM transactions from mutating @@ -1262,7 +1220,7 @@ pub fn execute_finalized_block( apply_evm_identity_plan( &mut tracking_copy, protocol_version, - identity_plan, + resolution.identity_plan, )?; } apply_evm_proposer_identity(&mut tracking_copy, protocol_version, &proposer)?; @@ -1438,7 +1396,7 @@ pub fn execute_finalized_block( // placing a hold on the correct purse. balance_identifier = BalanceIdentifier::Refund; Some(HandleRefundMode::RefundNoFeeCustomPayment { - initiator_addr: Box::new(runtime_origin.initiator_addr()?), + initiator_addr: Box::new(initiator_addr.clone()), limit: artifact_builder.limit(), gas_price: current_gas_price, cost: artifact_builder.cost_to_use(), @@ -1479,7 +1437,7 @@ pub fn execute_finalized_block( // logic, which is interpreted by inner logic to use the currently set // refund purse. Some(HandleRefundMode::Refund { - initiator_addr: Box::new(runtime_origin.initiator_addr()?), + initiator_addr: Box::new(initiator_addr.clone()), limit: artifact_builder.limit(), gas_price: current_gas_price, consumed, @@ -1590,7 +1548,9 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - runtime_origin.fee_initiator(), + initiator_addr + .account_hash() + .map(|_| Box::new(initiator_addr.clone())), balance_identifier, BalanceIdentifier::Public(*(proposer.clone())), fee_amount, @@ -1607,7 +1567,9 @@ pub fn execute_finalized_block( protocol_version, transaction_hash, HandleFeeMode::pay( - runtime_origin.fee_initiator(), + initiator_addr + .account_hash() + .map(|_| Box::new(initiator_addr.clone())), balance_identifier, BalanceIdentifier::Accumulate, fee_amount, diff --git a/node/src/components/contract_runtime/operations/wasm_v2_request.rs b/node/src/components/contract_runtime/operations/wasm_v2_request.rs index 68f7278ac9..487ba89bbd 100644 --- a/node/src/components/contract_runtime/operations/wasm_v2_request.rs +++ b/node/src/components/contract_runtime/operations/wasm_v2_request.rs @@ -89,6 +89,8 @@ pub(crate) enum InvalidRequest { ExpectedTransferredValue, #[error("Expected V2 runtime")] ExpectedV2Runtime, + #[error("Invalid initiator address")] + InvalidInitiatorAddr, } impl WasmV2Request { @@ -102,6 +104,9 @@ impl WasmV2Request { ) -> Result { let transaction_hash = transaction.hash(); let initiator_addr = transaction.initiator_addr(); + let initiator_account_hash = initiator_addr + .account_hash() + .ok_or(InvalidRequest::InvalidInitiatorAddr)?; let gas_limit: u64 = gas_limit .value() @@ -213,7 +218,6 @@ impl WasmV2Request { // different API. debug_assert_eq!(transferred_value, value); - let initiator_account_hash = initiator_addr.account_hash(); let install_request = builder .with_initiator(initiator_account_hash) .with_gas_limit(gas_limit) @@ -234,8 +238,6 @@ impl WasmV2Request { Target::Session { .. } | Target::Stored { .. } => { let mut builder = ExecuteRequestBuilder::default(); - let initiator_account_hash = initiator_addr.account_hash(); - let initiator_key = Key::Account(initiator_account_hash); builder = builder diff --git a/node/src/components/contract_runtime/types.rs b/node/src/components/contract_runtime/types.rs index cb82a5e81c..b53d0c9d40 100644 --- a/node/src/components/contract_runtime/types.rs +++ b/node/src/components/contract_runtime/types.rs @@ -75,7 +75,6 @@ pub(crate) struct ExecutionArtifactBuilder { messages: Messages, transfers: Vec, initiator: InitiatorAddr, - evm_initiator: Option, current_price: u8, cost: U512, limit: Gas, @@ -103,7 +102,6 @@ impl ExecutionArtifactBuilder { transfers: vec![], messages: Default::default(), initiator: transaction.initiator_addr(), - evm_initiator: transaction.evm_initiator_addr(), current_price, cost: initial_cost, limit, @@ -129,7 +127,6 @@ impl ExecutionArtifactBuilder { transfers: vec![], messages: Default::default(), initiator: transaction.initiator_addr(), - evm_initiator: transaction.evm_initiator_addr(), current_price, cost: U512::zero(), limit: Gas::zero(), @@ -494,7 +491,8 @@ impl ExecutionArtifactBuilder { let execution_result = if let Some(receipt) = self.evm_receipt { let result = EvmExecutionResult { initiator: self - .evm_initiator + .initiator + .evm_address() .expect("EVM execution result requires an EVM initiator"), current_price: self.current_price, limit: self.limit, diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index e84faea298..4097d34c8d 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -314,7 +314,15 @@ impl TransactionAcceptor { if event_metadata.source.is_client() { let initiator_addr = event_metadata.transaction.initiator_addr(); - let account_hash = initiator_addr.account_hash(); + let Some(account_hash) = initiator_addr.account_hash() else { + return self.reject_transaction( + effect_builder, + *event_metadata, + Error::InvalidTransaction(InvalidTransaction::V1( + InvalidTransactionV1::InvalidInitiatorAddr, + )), + ); + }; let entity_addr = EntityAddr::Account(account_hash.value()); effect_builder .get_addressable_entity(*block_header.state_root_hash(), entity_addr) diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 0173c149ee..0399871f9f 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -23,7 +23,7 @@ use casper_types::{ runtime_args, system::mint::{ARG_AMOUNT, ARG_TARGET}, AccessRights, AddressableEntity, CLValue, Digest, EntityAddr, ExecutableDeployItem, - ExecutionInfo, TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, + ExecutionInfo, InitiatorAddr, TransactionRuntimeParams, URef, URefAddr, DEFAULT_TRANSFER_COST, }; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use once_cell::sync::Lazy; @@ -1224,6 +1224,10 @@ async fn should_execute_evm_transaction_and_store_receipt() { &SigningKey::from_slice(&[0x11; 32]).unwrap(), )); assert_eq!(sender, expected_sender); + assert_eq!( + Transaction::from(evm_transaction.clone()).initiator_addr(), + InitiatorAddr::Eoa(sender) + ); let highest_block = test.fixture.highest_complete_block(); seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index 4fdfc8d882..6553fb2ea0 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -70,11 +70,11 @@ impl MetaTransaction { } /// Returns the Casper initiator address. - pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + pub(crate) fn initiator_addr(&self) -> InitiatorAddr { match self { - MetaTransaction::Deploy(meta_deploy) => meta_deploy.initiator_addr(), + MetaTransaction::Deploy(meta_deploy) => meta_deploy.initiator_addr().clone(), MetaTransaction::Evm(evm) => evm.initiator_addr(), - MetaTransaction::V1(txn) => txn.initiator_addr(), + MetaTransaction::V1(txn) => txn.initiator_addr().clone(), } } @@ -592,11 +592,7 @@ mod tests { #[test] fn evm_transaction_header_keeps_initiator_addr() { let evm_transaction = legacy_transaction(Some(CHAIN_ID), BASE_FEE_WEI, 21_000); - let expected_signer = evm_transaction - .signer() - .expect("signed EVM transaction should have an approval signer") - .clone(); - let expected_initiator_addr = InitiatorAddr::AccountHash(expected_signer.to_account_hash()); + let expected_initiator_addr = InitiatorAddr::Eoa(evm_transaction.from()); assert_eq!( Transaction::from_evm(evm_transaction.clone()).initiator_addr(), @@ -915,7 +911,6 @@ mod tests { EvmTransaction::new_unsigned_call( Timestamp::zero(), TimeDiff::from_seconds(60), - test_initiator_addr(), chain_id, evm::Address::new([1u8; 20]), Some(evm::Address::new([2u8; 20])), @@ -926,10 +921,6 @@ mod tests { ) } - fn test_initiator_addr() -> InitiatorAddr { - InitiatorAddr::AccountHash(AccountHash::new([8; 32])) - } - fn legacy_transaction( chain_id: Option, gas_price: u128, diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index 6b86e0aaa6..948b3af5af 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -55,7 +55,7 @@ impl MetaEvmTransaction { self.transaction.approval() } - pub(crate) fn initiator_addr(&self) -> &InitiatorAddr { + pub(crate) fn initiator_addr(&self) -> InitiatorAddr { self.transaction.initiator_addr() } diff --git a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs index 7437eebe89..79e3b83a81 100644 --- a/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs +++ b/node/src/types/transaction/meta_transaction/meta_transaction_v1.rs @@ -48,6 +48,12 @@ impl MetaTransactionV1 { v1: &TransactionV1, transaction_v1_config: &TransactionV1Config, ) -> Result { + if matches!(v1.initiator_addr(), InitiatorAddr::Eoa(_)) { + return Err(InvalidTransaction::V1( + InvalidTransactionV1::InvalidInitiatorAddr, + )); + } + let args_binary_len = v1 .payload() .fields() @@ -864,11 +870,34 @@ mod tests { use super::MetaTransactionV1; use crate::types::transaction::transaction_v1_builder::TransactionV1Builder; use casper_types::{ - testing::TestRng, InvalidTransaction, InvalidTransactionV1, PricingMode, SecretKey, - TransactionInvocationTarget, TransactionLaneDefinition, TransactionRuntimeParams, - TransactionV1Config, + evm::Address, testing::TestRng, InvalidTransaction, InvalidTransactionV1, PricingMode, + SecretKey, TransactionInvocationTarget, TransactionLaneDefinition, + TransactionRuntimeParams, TransactionV1Config, }; + #[test] + fn eoa_initiator_should_be_rejected() { + let rng = &mut TestRng::new(); + let secret_key = SecretKey::random(rng); + let transaction_v1 = TransactionV1Builder::new_session( + false, + vec![1; 30].into(), + TransactionRuntimeParams::VmCasperV1, + ) + .with_chain_name("x".to_string()) + .with_initiator_addr(Address::new([7; 20])) + .with_secret_key(&secret_key) + .build() + .unwrap(); + + assert!(matches!( + MetaTransactionV1::from_transaction_v1(&transaction_v1, &build_v1_config()), + Err(InvalidTransaction::V1( + InvalidTransactionV1::InvalidInitiatorAddr + )) + )); + } + #[test] fn limited_amount_should_determine_transaction_lane_for_session() { let rng = &mut TestRng::new(); diff --git a/node/src/types/transaction/meta_transaction/transaction_header.rs b/node/src/types/transaction/meta_transaction/transaction_header.rs index a60aebbf33..20a59ab1bb 100644 --- a/node/src/types/transaction/meta_transaction/transaction_header.rs +++ b/node/src/types/transaction/meta_transaction/transaction_header.rs @@ -95,7 +95,7 @@ impl From<&TransactionV1> for TransactionHeader { impl From<&EvmTransaction> for TransactionHeader { fn from(transaction: &EvmTransaction) -> Self { let meta = EvmTransactionMetadata { - initiator_addr: transaction.initiator_addr().clone(), + initiator_addr: transaction.initiator_addr(), timestamp: transaction.timestamp(), ttl: transaction.ttl(), }; diff --git a/resources/test/sse_data_schema.json b/resources/test/sse_data_schema.json index e0d4cdee21..048a0983f3 100644 --- a/resources/test/sse_data_schema.json +++ b/resources/test/sse_data_schema.json @@ -1675,6 +1675,19 @@ } }, "additionalProperties": false + }, + { + "description": "An externally-owned Ethereum account address.", + "type": "object", + "required": [ + "Eoa" + ], + "properties": { + "Eoa": { + "$ref": "#/definitions/Address" + } + }, + "additionalProperties": false } ] }, @@ -1682,6 +1695,10 @@ "description": "Account hash as a formatted string.", "type": "string" }, + "Address": { + "description": "A 20-byte Ethereum account or contract address encoded as 0x-prefixed hexadecimal.", + "type": "string" + }, "PricingMode": { "description": "Pricing mode of a Transaction.", "oneOf": [ @@ -1791,7 +1808,6 @@ "from", "gas_limit", "hash", - "initiator_addr", "input", "kind", "max_fee_per_gas", @@ -1807,9 +1823,6 @@ "ttl": { "$ref": "#/definitions/TimeDiff" }, - "initiator_addr": { - "$ref": "#/definitions/InitiatorAddr" - }, "hash": { "$ref": "#/definitions/EvmTransactionHash" }, @@ -1897,10 +1910,6 @@ } } }, - "Address": { - "description": "A 20-byte Ethereum account or contract address encoded as hexadecimal.", - "type": "string" - }, "EvmTransactionKind": { "description": "The supported Ethereum transaction envelope kinds.", "oneOf": [ @@ -5344,6 +5353,44 @@ } } }, + "RetValue": { + "description": "Type disambiguating between the formatting of the returned data.", + "oneOf": [ + { + "description": "The returned data is CLValue.", + "type": "object", + "required": [ + "CLValue" + ], + "properties": { + "CLValue": { + "$ref": "#/definitions/CLValue" + } + }, + "additionalProperties": false + }, + { + "description": "The returned data is serialized bytes.", + "type": "object", + "required": [ + "Bytes" + ], + "properties": { + "Bytes": { + "$ref": "#/definitions/Bytes" + } + }, + "additionalProperties": false + }, + { + "description": "There was no returned data.", + "type": "string", + "enum": [ + "Unit" + ] + } + ] + }, "EvmExecutionResult": { "description": "The result of executing a single EVM transaction.", "type": "object", @@ -5741,44 +5788,6 @@ "description": "A 32-byte EVM log topic encoded as 0x-prefixed hexadecimal.", "type": "string" }, - "RetValue": { - "description": "Type disambiguating between the formatting of the returned data.", - "oneOf": [ - { - "description": "The returned data is CLValue.", - "type": "object", - "required": [ - "CLValue" - ], - "properties": { - "CLValue": { - "$ref": "#/definitions/CLValue" - } - }, - "additionalProperties": false - }, - { - "description": "The returned data is serialized bytes.", - "type": "object", - "required": [ - "Bytes" - ], - "properties": { - "Bytes": { - "$ref": "#/definitions/Bytes" - } - }, - "additionalProperties": false - }, - { - "description": "There was no returned data.", - "type": "string", - "enum": [ - "Unit" - ] - } - ] - }, "Message": { "description": "Message that was emitted by an addressable entity during execution.", "type": "object", diff --git a/storage/src/data_access_layer.rs b/storage/src/data_access_layer.rs index a34c16442e..7af13031ca 100644 --- a/storage/src/data_access_layer.rs +++ b/storage/src/data_access_layer.rs @@ -56,8 +56,8 @@ mod trie; pub use addressable_entity::{AddressableEntityRequest, AddressableEntityResult}; pub use auction::{AuctionMethod, BiddingRequest, BiddingResult}; pub use balance::{ - BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceRequest, BalanceResult, - GasHoldBalanceHandling, ProofHandling, ProofsResult, + BalanceHolds, BalanceHoldsWithProof, BalanceIdentifier, BalanceIdentifierFromInitiatorError, + BalanceRequest, BalanceResult, GasHoldBalanceHandling, ProofHandling, ProofsResult, }; pub use balance_hold::{ BalanceHoldError, BalanceHoldKind, BalanceHoldMode, BalanceHoldRequest, BalanceHoldResult, diff --git a/storage/src/data_access_layer/balance.rs b/storage/src/data_access_layer/balance.rs index 67185f2d22..650c92b309 100644 --- a/storage/src/data_access_layer/balance.rs +++ b/storage/src/data_access_layer/balance.rs @@ -1,6 +1,7 @@ //! Types for balance queries. use casper_types::{ account::AccountHash, + evm, global_state::TrieMerkleProof, system::{ handle_payment::{ACCUMULATION_PURSE_KEY, PAYMENT_PURSE_KEY, REFUND_PURSE_KEY}, @@ -15,6 +16,7 @@ use num_rational::Ratio; use num_traits::CheckedMul; use std::{ collections::{btree_map::Entry, BTreeMap}, + convert::TryFrom, fmt::{Display, Formatter}, }; use tracing::error; @@ -70,11 +72,34 @@ pub enum BalanceIdentifier { PenalizedPayment, } -impl From for BalanceIdentifier { - fn from(value: InitiatorAddr) -> Self { +/// Error converting a transaction initiator into a balance identifier. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum BalanceIdentifierFromInitiatorError { + /// EOA initiators require state-aware EVM origin resolution. + Eoa(evm::Address), +} + +impl Display for BalanceIdentifierFromInitiatorError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BalanceIdentifierFromInitiatorError::Eoa(address) => write!( + formatter, + "EOA initiator address {address:?} cannot directly identify a balance" + ), + } + } +} + +impl TryFrom for BalanceIdentifier { + type Error = BalanceIdentifierFromInitiatorError; + + fn try_from(value: InitiatorAddr) -> Result { match value { - InitiatorAddr::PublicKey(public_key) => BalanceIdentifier::Public(public_key), - InitiatorAddr::AccountHash(account_hash) => BalanceIdentifier::Account(account_hash), + InitiatorAddr::PublicKey(public_key) => Ok(BalanceIdentifier::Public(public_key)), + InitiatorAddr::AccountHash(account_hash) => { + Ok(BalanceIdentifier::Account(account_hash)) + } + InitiatorAddr::Eoa(address) => Err(BalanceIdentifierFromInitiatorError::Eoa(address)), } } } diff --git a/storage/src/global_state/state/mod.rs b/storage/src/global_state/state/mod.rs index d17850bd59..924fb22120 100644 --- a/storage/src/global_state/state/mod.rs +++ b/storage/src/global_state/state/mod.rs @@ -1171,7 +1171,9 @@ pub trait StateProvider: Send + Sync + Sized { Err(err) => return BiddingResult::Failure(TrackingCopyError::Storage(err)), }; - let source_account_hash = initiator.account_hash(); + let Some(source_account_hash) = initiator.account_hash() else { + return BiddingResult::Failure(TrackingCopyError::Authorization); + }; let (entity_addr, mut footprint, mut entity_access_rights) = match tc .borrow_mut() .authorized_runtime_footprint_with_access_rights( @@ -1487,10 +1489,13 @@ pub trait StateProvider: Send + Sync + Sized { Ok(value) => value, Err(tce) => return HandleRefundResult::Failure(tce), }; + let Some(initiator_account_hash) = initiator_addr.account_hash() else { + return HandleRefundResult::Failure(TrackingCopyError::Authorization); + }; // pay amount from source to target match runtime .transfer( - Some(initiator_addr.account_hash()), + Some(initiator_account_hash), source_purse, target_purse, refund_amount, @@ -1558,9 +1563,12 @@ pub trait StateProvider: Send + Sync + Sized { Ok(value) => value, Err(tce) => return HandleRefundResult::Failure(tce), }; + let Some(initiator_account_hash) = initiator_addr.account_hash() else { + return HandleRefundResult::Failure(TrackingCopyError::Authorization); + }; match runtime .transfer( - Some(initiator_addr.account_hash()), + Some(initiator_account_hash), source_purse, target_purse, refund_amount, @@ -1704,7 +1712,7 @@ pub trait StateProvider: Send + Sync + Sized { .transfer( initiator_addr .as_ref() - .map(|initiator_addr| initiator_addr.account_hash()), + .and_then(|initiator_addr| initiator_addr.account_hash()), source_purse, target_purse, amount, @@ -2081,7 +2089,11 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request.initiator().account_hash(); + let Some(source_account_hash) = request.initiator().account_hash() else { + return TransferResult::Failure(TransferError::TrackingCopy( + TrackingCopyError::Authorization, + )); + }; let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() @@ -2322,7 +2334,9 @@ pub trait StateProvider: Send + Sync + Sized { } }; - let source_account_hash = request.initiator().account_hash(); + let Some(source_account_hash) = request.initiator().account_hash() else { + return BurnResult::Failure(BurnError::TrackingCopy(TrackingCopyError::Authorization)); + }; let protocol_version = request.protocol_version(); if let Err(tce) = tc .borrow_mut() diff --git a/types/src/evm/address.rs b/types/src/evm/address.rs index bd655289a2..8fe5fd94fd 100644 --- a/types/src/evm/address.rs +++ b/types/src/evm/address.rs @@ -8,7 +8,7 @@ use core::{ use datasize::DataSize; #[cfg(feature = "json-schema")] use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer}; use alloy_primitives::keccak256; @@ -23,9 +23,7 @@ pub const ADDRESS_LENGTH: usize = 20; const ADDRESS_SERIALIZED_LENGTH: usize = ADDRESS_LENGTH; /// A 20-byte Ethereum account or contract address. -#[derive( - Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize, -)] +#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] pub struct Address([u8; ADDRESS_LENGTH]); @@ -103,6 +101,33 @@ impl Display for Address { } } +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.collect_str(self) + } else { + self.0.serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for Address { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let value = String::deserialize(deserializer)?; + let hex = value + .strip_prefix("0x") + .ok_or_else(|| D::Error::custom("address must start with 0x"))?; + let bytes = base16::decode(hex.as_bytes()).map_err(SerdeError::custom)?; + let bytes = + <[u8; ADDRESS_LENGTH]>::try_from(bytes.as_ref()).map_err(SerdeError::custom)?; + Ok(Address::new(bytes)) + } else { + <[u8; ADDRESS_LENGTH]>::deserialize(deserializer).map(Address::new) + } + } +} + #[cfg(feature = "json-schema")] impl JsonSchema for Address { fn schema_name() -> String { @@ -112,8 +137,10 @@ impl JsonSchema for Address { fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { let schema = gen.subschema_for::(); let mut schema_object = schema.into_object(); - schema_object.metadata().description = - Some("A 20-byte Ethereum account or contract address encoded as hexadecimal.".into()); + schema_object.metadata().description = Some( + "A 20-byte Ethereum account or contract address encoded as 0x-prefixed hexadecimal." + .into(), + ); schema_object.into() } } @@ -156,6 +183,30 @@ mod tests { use super::*; use crate::{CLValue, SecretKey}; + #[test] + fn human_readable_serde_uses_0x_prefixed_hex() { + let address = Address::new([0xab; ADDRESS_LENGTH]); + let expected_hex = "ab".repeat(ADDRESS_LENGTH); + + let encoded = serde_json::to_value(address).expect("address should serialize"); + assert_eq!(encoded, serde_json::json!(format!("0x{expected_hex}"))); + + let decoded: Address = serde_json::from_value(encoded).expect("address should deserialize"); + assert_eq!(decoded, address); + + assert!(serde_json::from_value::
(serde_json::json!(expected_hex)).is_err()); + assert!(serde_json::from_value::
(serde_json::json!("0xab")).is_err()); + } + + #[test] + fn non_human_readable_serde_roundtrip() { + let address = Address::new([0xcd; ADDRESS_LENGTH]); + let encoded = bincode::serialize(&address).expect("address should serialize"); + let decoded: Address = bincode::deserialize(&encoded).expect("address should deserialize"); + + assert_eq!(decoded, address); + } + #[test] fn evm_address_cl_value_roundtrip() { let address = Address::new([0x11; ADDRESS_LENGTH]); diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 73766e9187..2e870e513b 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -46,7 +46,7 @@ use crate::{ }; const TRANSACTION_KIND_SERIALIZED_LENGTH: usize = U8_SERIALIZED_LENGTH; -const EVM_TRANSACTION_MAX_CURRENT_FIELDS: u32 = 16; +const EVM_TRANSACTION_MAX_CURRENT_FIELDS: u32 = 15; const TIMESTAMP_FIELD_INDEX: u16 = 0; const TTL_FIELD_INDEX: u16 = 1; @@ -76,7 +76,6 @@ const DYNAMIC_APPROVAL_FIELD_INDEX: u16 = 13; const EIP7702_AUTHORIZATION_LIST_FIELD_INDEX: u16 = 13; const EIP7702_APPROVAL_FIELD_INDEX: u16 = 14; -const INITIATOR_ADDR_FIELD_INDEX: u16 = 15; /// Ethereum transaction type ID for legacy transactions. pub const LEGACY_TRANSACTION_TYPE_ID: u8 = 0; @@ -623,7 +622,6 @@ impl std::error::Error for EvmTransactionError {} pub struct EvmTransaction { timestamp: Timestamp, ttl: TimeDiff, - initiator_addr: InitiatorAddr, hash: EvmTransactionHash, from: Address, kind: EvmTransactionKind, @@ -653,7 +651,6 @@ pub struct EvmTransaction { struct EvmTransactionSerHelper<'a> { timestamp: Timestamp, ttl: TimeDiff, - initiator_addr: &'a InitiatorAddr, hash: EvmTransactionHash, from: Address, kind: EvmTransactionKind, @@ -675,7 +672,6 @@ struct EvmTransactionSerHelper<'a> { struct EvmTransactionDeserHelper { timestamp: Timestamp, ttl: TimeDiff, - initiator_addr: InitiatorAddr, hash: EvmTransactionHash, from: Address, kind: EvmTransactionKind, @@ -698,7 +694,6 @@ impl Serialize for EvmTransaction { EvmTransactionSerHelper { timestamp: self.timestamp, ttl: self.ttl, - initiator_addr: &self.initiator_addr, hash: self.hash, from: self.from, kind: self.kind, @@ -725,7 +720,6 @@ impl<'de> Deserialize<'de> for EvmTransaction { let transaction = EvmTransaction { timestamp: helper.timestamp, ttl: helper.ttl, - initiator_addr: helper.initiator_addr, hash: helper.hash, from: helper.from, kind: helper.kind, @@ -758,7 +752,6 @@ impl EvmTransaction { pub fn new_unsigned_call( timestamp: Timestamp, ttl: TimeDiff, - initiator_addr: InitiatorAddr, chain_id: u64, from: Address, to: Option
, @@ -770,7 +763,6 @@ impl EvmTransaction { let mut transaction = EvmTransaction { timestamp, ttl, - initiator_addr, hash: EvmTransactionHash::default(), from, kind: EvmTransactionKind::Legacy, @@ -799,9 +791,6 @@ impl EvmTransaction { self.ttl .write_bytes(&mut bytes) .expect("ttl should serialize"); - self.initiator_addr - .write_bytes(&mut bytes) - .expect("initiator address should serialize"); self.from .write_bytes(&mut bytes) .expect("from address should serialize"); @@ -846,7 +835,6 @@ impl EvmTransaction { input_length, self.chain_id.serialized_length(), self.approval.serialized_length(), - self.initiator_addr.serialized_length(), ]); } EvmTransactionKind::Eip1559 => { @@ -857,7 +845,6 @@ impl EvmTransaction { input_length, self.chain_id.serialized_length(), self.approval.serialized_length(), - self.initiator_addr.serialized_length(), ]); } EvmTransactionKind::Eip7702 => { @@ -869,7 +856,6 @@ impl EvmTransaction { self.chain_id.serialized_length(), self.authorization_list.serialized_length(), self.approval.serialized_length(), - self.initiator_addr.serialized_length(), ]); } } @@ -943,8 +929,6 @@ impl EvmTransaction { }; let signature_hash = envelope.signature_hash(); let approval = evm_approval_from_alloy_signature(envelope.signature(), &signature_hash)?; - let initiator_addr = InitiatorAddr::AccountHash(approval.signer().to_account_hash()); - let from = envelope .recover_signer() .map_err(|error| EvmTransactionError::SenderRecovery(format!("{error:?}")))?; @@ -967,7 +951,6 @@ impl EvmTransaction { Ok(EvmTransaction { timestamp, ttl, - initiator_addr, hash: b256_to_transaction_hash(*envelope.tx_hash()), from: alloy_address_to_address(from), kind, @@ -1024,7 +1007,6 @@ impl EvmTransaction { let signature = Signature::secp256k1(signature_bytes) .map_err(|_| EvmTransactionError::InvalidApprovalSignature)?; let signer = PublicKey::from(secret_key); - let initiator_addr = InitiatorAddr::AccountHash(signer.to_account_hash()); let y_parity = recovery_id.is_y_odd(); let approval = EvmApproval::new(Approval::new(signer, signature), y_parity); @@ -1033,7 +1015,6 @@ impl EvmTransaction { let signed = unsigned.into_envelope(alloy_signature); self.approval = Some(approval); - self.initiator_addr = initiator_addr; self.from = evm_address_from_verifying_key(&recovered_key); self.hash = b256_to_transaction_hash(*signed.tx_hash()); self.verify() @@ -1079,9 +1060,9 @@ impl EvmTransaction { .signer()) } - /// Returns the Casper initiator address attached to this EVM transaction. - pub fn initiator_addr(&self) -> &InitiatorAddr { - &self.initiator_addr + /// Returns the initiator address derived from this EVM transaction's sender. + pub fn initiator_addr(&self) -> InitiatorAddr { + InitiatorAddr::Eoa(self.from) } /// Returns this transaction with a replacement EVM approval. @@ -1356,10 +1337,6 @@ impl EvmTransaction { if &recovered_public_key != expected_signer { return Err(EvmTransactionError::InvalidApprovalSignature); } - let expected_initiator_addr = InitiatorAddr::AccountHash(expected_signer.to_account_hash()); - if self.initiator_addr != expected_initiator_addr { - return Err(EvmTransactionError::InvalidApprovalSignature); - } Ok(( alloy_signature, evm_address_from_verifying_key(&recovered_key), @@ -1399,7 +1376,6 @@ impl ToBytes for EvmTransaction { .add_field(LEGACY_INPUT_FIELD_INDEX, &input)? .add_field(LEGACY_CHAIN_ID_FIELD_INDEX, &self.chain_id)? .add_field(LEGACY_APPROVAL_FIELD_INDEX, &self.approval)? - .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? .binary_payload_bytes() } EvmTransactionKind::Eip1559 => { @@ -1414,7 +1390,6 @@ impl ToBytes for EvmTransaction { .add_field(DYNAMIC_INPUT_FIELD_INDEX, &input)? .add_field(DYNAMIC_CHAIN_ID_FIELD_INDEX, &self.chain_id)? .add_field(DYNAMIC_APPROVAL_FIELD_INDEX, &self.approval)? - .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? .binary_payload_bytes() } EvmTransactionKind::Eip7702 => { @@ -1433,7 +1408,6 @@ impl ToBytes for EvmTransaction { &self.authorization_list, )? .add_field(EIP7702_APPROVAL_FIELD_INDEX, &self.approval)? - .add_field(INITIATOR_ADDR_FIELD_INDEX, &self.initiator_addr)? .binary_payload_bytes() } } @@ -1499,10 +1473,6 @@ impl EvmTransaction { window.verify_index(LEGACY_APPROVAL_FIELD_INDEX)?; let (approval, window) = window.deserialize_and_maybe_next::>()?; - let window = window.ok_or(bytesrepr::Error::Formatting)?; - window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; - let (initiator_addr, window) = - window.deserialize_and_maybe_next::()?; if window.is_some() { return Err(bytesrepr::Error::Formatting); } @@ -1514,7 +1484,6 @@ impl EvmTransaction { EvmTransaction { timestamp, ttl, - initiator_addr, hash, from, kind, @@ -1552,17 +1521,12 @@ impl EvmTransaction { window.verify_index(DYNAMIC_APPROVAL_FIELD_INDEX)?; let (approval, window) = window.deserialize_and_maybe_next::>()?; - let window = window.ok_or(bytesrepr::Error::Formatting)?; - window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; - let (initiator_addr, window) = - window.deserialize_and_maybe_next::()?; if window.is_some() { return Err(bytesrepr::Error::Formatting); } EvmTransaction { timestamp, ttl, - initiator_addr, hash, from, kind, @@ -1604,17 +1568,12 @@ impl EvmTransaction { window.verify_index(EIP7702_APPROVAL_FIELD_INDEX)?; let (approval, window) = window.deserialize_and_maybe_next::>()?; - let window = window.ok_or(bytesrepr::Error::Formatting)?; - window.verify_index(INITIATOR_ADDR_FIELD_INDEX)?; - let (initiator_addr, window) = - window.deserialize_and_maybe_next::()?; if window.is_some() { return Err(bytesrepr::Error::Formatting); } EvmTransaction { timestamp, ttl, - initiator_addr, hash, from, kind, @@ -1801,12 +1760,12 @@ mod tests { #[test] fn unsigned_call_transaction_bytesrepr_roundtrips_without_approvals() { + let from = Address::new([1; crate::evm::ADDRESS_LENGTH]); let transaction = EvmTransaction::new_unsigned_call( Timestamp::zero(), TimeDiff::from_seconds(300), - test_initiator_addr(), 7, - Address::new([1; crate::evm::ADDRESS_LENGTH]), + from, Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), U256::from(3), vec![0xde, 0xad], @@ -1815,6 +1774,7 @@ mod tests { ); assert!(transaction.approval().is_none()); + assert_eq!(transaction.initiator_addr(), InitiatorAddr::Eoa(from)); assert!(transaction.is_unsigned_call()); assert!(matches!( transaction.verify(), @@ -1827,6 +1787,10 @@ mod tests { fn signed_legacy_transaction_bytesrepr_roundtrips() { let transaction = signed_legacy_transaction(); + assert_eq!( + transaction.initiator_addr(), + InitiatorAddr::Eoa(transaction.from()) + ); assert_eq!( transaction.max_fee_per_gas(), transaction @@ -1841,7 +1805,6 @@ mod tests { let mut transaction = EvmTransaction::new_unsigned_call( Timestamp::zero(), TimeDiff::from_seconds(300), - test_initiator_addr(), 7, Address::new([1; crate::evm::ADDRESS_LENGTH]), Some(Address::new([2; crate::evm::ADDRESS_LENGTH])), @@ -1885,10 +1848,6 @@ mod tests { .expect("transaction should decode") } - fn test_initiator_addr() -> InitiatorAddr { - InitiatorAddr::AccountHash(crate::account::AccountHash::new([9; 32])) - } - fn signed_eip7702_transaction() -> EvmTransaction { let authorization = AlloyAuthorization { chain_id: AlloyU256::from(7), diff --git a/types/src/gens.rs b/types/src/gens.rs index 302a6bd4ae..62956a9e6d 100644 --- a/types/src/gens.rs +++ b/types/src/gens.rs @@ -1320,6 +1320,8 @@ pub fn initiator_addr_arb() -> impl Strategy { prop_oneof![ public_key_arb_no_system().prop_map(InitiatorAddr::PublicKey), u2_slice_32().prop_map(|hash| InitiatorAddr::AccountHash(AccountHash::new(hash))), + any::<[u8; crate::evm::ADDRESS_LENGTH]>() + .prop_map(|address| InitiatorAddr::Eoa(crate::evm::Address::new(address))), ] } diff --git a/types/src/transaction.rs b/types/src/transaction.rs index 5081dc1057..3b25b0a910 100644 --- a/types/src/transaction.rs +++ b/types/src/transaction.rs @@ -311,7 +311,7 @@ impl Transaction { match self { Transaction::Deploy(deploy) => InitiatorAddr::PublicKey(deploy.account().clone()), Transaction::V1(txn) => txn.initiator_addr().clone(), - Transaction::Evm(txn) => txn.initiator_addr().clone(), + Transaction::Evm(txn) => txn.initiator_addr(), } } diff --git a/types/src/transaction/deploy.rs b/types/src/transaction/deploy.rs index 10d2e13b0c..ee191bfd1c 100644 --- a/types/src/transaction/deploy.rs +++ b/types/src/transaction/deploy.rs @@ -218,7 +218,7 @@ impl Deploy { let account = match initiator_addr_and_secret_key.initiator_addr() { InitiatorAddr::PublicKey(public_key) => public_key, - InitiatorAddr::AccountHash(_) => unreachable!(), + InitiatorAddr::AccountHash(_) | InitiatorAddr::Eoa(_) => unreachable!(), }; let dependencies = dependencies.into_iter().unique().collect(); diff --git a/types/src/transaction/initiator_addr.rs b/types/src/transaction/initiator_addr.rs index 215b035125..59d31879e2 100644 --- a/types/src/transaction/initiator_addr.rs +++ b/types/src/transaction/initiator_addr.rs @@ -7,6 +7,7 @@ use crate::{ Error::{self, Formatting}, FromBytes, ToBytes, }, + evm::Address, transaction::serialization::CalltableSerializationEnvelopeBuilder, AsymmetricType, PublicKey, }; @@ -28,6 +29,9 @@ const PUBLIC_KEY_FIELD_INDEX: u16 = 1; const ACCOUNT_HASH_VARIANT_TAG: u8 = 1; const ACCOUNT_HASH_FIELD_INDEX: u16 = 1; +const EOA_VARIANT_TAG: u8 = 2; +const EOA_FIELD_INDEX: u16 = 1; + /// The address of the initiator of a [`crate::Transaction`]. #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] @@ -42,23 +46,35 @@ pub enum InitiatorAddr { PublicKey(PublicKey), /// The account hash derived from the public key of the initiator. AccountHash(AccountHash), + /// An externally-owned Ethereum account address. + Eoa(Address), } impl InitiatorAddr { - /// Returns the Casper account hash carried by this initiator. - pub fn account_hash(&self) -> AccountHash { + /// Returns the Casper account hash carried by this initiator, if any. + pub fn account_hash(&self) -> Option { + match self { + InitiatorAddr::PublicKey(public_key) => Some(public_key.to_account_hash()), + InitiatorAddr::AccountHash(hash) => Some(*hash), + InitiatorAddr::Eoa(_) => None, + } + } + + /// Returns the EVM address carried by this initiator, if any. + pub fn evm_address(&self) -> Option
{ match self { - InitiatorAddr::PublicKey(public_key) => public_key.to_account_hash(), - InitiatorAddr::AccountHash(hash) => *hash, + InitiatorAddr::Eoa(address) => Some(*address), + InitiatorAddr::PublicKey(_) | InitiatorAddr::AccountHash(_) => None, } } /// Returns a random `InitiatorAddr`. #[cfg(any(feature = "testing", test))] pub fn random(rng: &mut TestRng) -> Self { - match rng.gen_range(0..=1) { + match rng.gen_range(0..=2) { 0 => InitiatorAddr::PublicKey(PublicKey::random(rng)), 1 => InitiatorAddr::AccountHash(rng.gen()), + 2 => InitiatorAddr::Eoa(Address::new(rng.gen())), _ => unreachable!(), } } @@ -77,6 +93,12 @@ impl InitiatorAddr { hash.serialized_length(), ] } + InitiatorAddr::Eoa(address) => { + vec![ + crate::bytesrepr::U8_SERIALIZED_LENGTH, + address.serialized_length(), + ] + } } } } @@ -96,6 +118,12 @@ impl ToBytes for InitiatorAddr { .add_field(ACCOUNT_HASH_FIELD_INDEX, &hash)? .binary_payload_bytes() } + InitiatorAddr::Eoa(address) => { + CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())? + .add_field(TAG_FIELD_INDEX, &EOA_VARIANT_TAG)? + .add_field(EOA_FIELD_INDEX, &address)? + .binary_payload_bytes() + } } } fn serialized_length(&self) -> usize { @@ -128,6 +156,15 @@ impl FromBytes for InitiatorAddr { } Ok(InitiatorAddr::AccountHash(hash)) } + EOA_VARIANT_TAG => { + let window = window.ok_or(Formatting)?; + window.verify_index(EOA_FIELD_INDEX)?; + let (address, window) = window.deserialize_and_maybe_next::
()?; + if window.is_some() { + return Err(Formatting); + } + Ok(InitiatorAddr::Eoa(address)) + } _ => Err(Formatting), }; to_ret.map(|endpoint| (endpoint, remainder)) @@ -146,6 +183,12 @@ impl From for InitiatorAddr { } } +impl From
for InitiatorAddr { + fn from(address: Address) -> Self { + InitiatorAddr::Eoa(address) + } +} + impl Display for InitiatorAddr { fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { match self { @@ -155,6 +198,7 @@ impl Display for InitiatorAddr { InitiatorAddr::AccountHash(account_hash) => { write!(formatter, "account hash {}", account_hash) } + InitiatorAddr::Eoa(address) => write!(formatter, "EOA {}", address), } } } @@ -170,6 +214,7 @@ impl Debug for InitiatorAddr { .debug_tuple("AccountHash") .field(account_hash) .finish(), + InitiatorAddr::Eoa(address) => formatter.debug_tuple("Eoa").field(address).finish(), } } } @@ -188,6 +233,61 @@ mod tests { } } + #[test] + fn variant_tags_are_stable() { + let rng = &mut TestRng::new(); + let public_key = InitiatorAddr::PublicKey(PublicKey::random(rng)); + let account_hash = InitiatorAddr::AccountHash(AccountHash::new([1; 32])); + let eoa = InitiatorAddr::Eoa(Address::new([2; crate::evm::ADDRESS_LENGTH])); + + assert_eq!(serialized_tag(&public_key), 0); + assert_eq!(serialized_tag(&account_hash), 1); + assert_eq!(serialized_tag(&eoa), 2); + } + + #[test] + fn eoa_accessors_do_not_fabricate_an_account_hash() { + let address = Address::new([3; crate::evm::ADDRESS_LENGTH]); + let initiator = InitiatorAddr::from(address); + + assert_eq!(initiator.account_hash(), None); + assert_eq!(initiator.evm_address(), Some(address)); + assert_eq!( + InitiatorAddr::AccountHash(AccountHash::new([4; 32])).evm_address(), + None + ); + } + + #[test] + fn eoa_serde_roundtrip() { + let initiator = InitiatorAddr::Eoa(Address::new([5; crate::evm::ADDRESS_LENGTH])); + let json = serde_json::to_string(&initiator).expect("should serialize EOA initiator"); + assert_eq!( + json, + r#"{"Eoa":"0x0505050505050505050505050505050505050505"}"# + ); + let decoded = + serde_json::from_str::(&json).expect("should deserialize EOA initiator"); + + assert_eq!(decoded, initiator); + } + + fn serialized_tag(initiator: &InitiatorAddr) -> u8 { + let bytes = initiator.to_bytes().expect("initiator should serialize"); + let (payload, remainder) = + CalltableSerializationEnvelope::from_bytes(2, &bytes).expect("valid calltable"); + assert!(remainder.is_empty()); + let window = payload + .start_consuming() + .expect("valid fields") + .expect("tag field"); + window.verify_index(TAG_FIELD_INDEX).expect("tag index"); + window + .deserialize_and_maybe_next::() + .expect("tag should deserialize") + .0 + } + proptest! { #[test] fn generative_bytesrepr_roundtrip(val in initiator_addr_arb()) { diff --git a/types/src/transaction/transaction_v1.rs b/types/src/transaction/transaction_v1.rs index 41d4b7ac30..3f4e38570b 100644 --- a/types/src/transaction/transaction_v1.rs +++ b/types/src/transaction/transaction_v1.rs @@ -438,6 +438,11 @@ impl TransactionV1 { } fn do_verify(&self) -> Result<(), InvalidTransactionV1> { + if self.initiator_addr().account_hash().is_none() { + trace!(?self, "transaction has an invalid V1 initiator address"); + return Err(InvalidTransactionV1::InvalidInitiatorAddr); + } + if self.approvals.is_empty() { trace!(?self, "transaction has no approvals"); return Err(InvalidTransactionV1::EmptyApprovals); diff --git a/types/src/transaction/transaction_v1/errors_v1.rs b/types/src/transaction/transaction_v1/errors_v1.rs index 814803c168..c41d755d25 100644 --- a/types/src/transaction/transaction_v1/errors_v1.rs +++ b/types/src/transaction/transaction_v1/errors_v1.rs @@ -83,6 +83,9 @@ pub enum InvalidTransaction { /// The provided transaction hash does not match the actual hash of the transaction. InvalidTransactionHash, + /// The transaction uses an initiator address that is not valid for a V1 transaction. + InvalidInitiatorAddr, + /// The transaction has no approvals. EmptyApprovals, @@ -325,6 +328,12 @@ impl Display for InvalidTransaction { "the provided hash does not match the actual hash of the transaction" ) } + InvalidTransaction::InvalidInitiatorAddr => { + write!( + formatter, + "the transaction initiator must be a Casper public key or account hash" + ) + } InvalidTransaction::EmptyApprovals => { write!(formatter, "the transaction has no approvals") } @@ -549,6 +558,7 @@ impl StdError for InvalidTransaction { | InvalidTransaction::TimestampInFuture { .. } | InvalidTransaction::InvalidBodyHash | InvalidTransaction::InvalidTransactionHash + | InvalidTransaction::InvalidInitiatorAddr | InvalidTransaction::EmptyApprovals | InvalidTransaction::ExcessiveArgsLength { .. } | InvalidTransaction::ExcessiveApprovals { .. } diff --git a/types/tests/evm_transaction.rs b/types/tests/evm_transaction.rs index 9033437824..d4e1df226b 100644 --- a/types/tests/evm_transaction.rs +++ b/types/tests/evm_transaction.rs @@ -16,8 +16,8 @@ use casper_types::{ bytesrepr::{FromBytes, ToBytes}, evm::{Address, Hash, EIP4844_TRANSACTION_TYPE_ID}, Approval, ApprovalsHash, Digest, EvmApproval, EvmTransaction, EvmTransactionError, - EvmTransactionHash, EvmTransactionKind, PublicKey, SecretKey, TimeDiff, Timestamp, - Transaction as CasperTransaction, TransactionHash, U256, + EvmTransactionHash, EvmTransactionKind, InitiatorAddr, PublicKey, SecretKey, TimeDiff, + Timestamp, Transaction as CasperTransaction, TransactionHash, U256, }; const SIGNING_SECRET: [u8; 32] = [7; 32]; @@ -36,6 +36,7 @@ fn decodes_legacy_signed_rlp() { assert_eq!(transaction.gas_price(), Some(1_000_000_000)); assert_eq!(transaction.value(), U256::from(123u64)); assert_eq!(transaction.chain_id(), Some(7)); + assert_derived_initiator(&transaction); transaction .verify() .expect("legacy transaction should verify"); @@ -60,6 +61,7 @@ fn decodes_eip2930_signed_rlp() { assert_eq!(transaction.value(), U256::from(456u64)); assert_eq!(transaction.input(), &[0x12, 0x34]); assert_eq!(transaction.chain_id(), Some(7)); + assert_derived_initiator(&transaction); transaction .verify() .expect("EIP-2930 transaction should verify"); @@ -81,6 +83,7 @@ fn decodes_eip1559_signed_rlp() { assert_eq!(transaction.value(), U256::from(789u64)); assert_eq!(transaction.input(), &[0xab, 0xcd]); assert_eq!(transaction.chain_id(), Some(7)); + assert_derived_initiator(&transaction); transaction .verify() .expect("EIP-1559 transaction should verify"); @@ -103,6 +106,7 @@ fn decodes_eip7702_signed_rlp() { assert_eq!(transaction.input(), &[0xde, 0xad]); assert_eq!(transaction.chain_id(), Some(7)); assert_eq!(transaction.authorization_list().len(), 1); + assert_derived_initiator(&transaction); let expected = &signed_transaction.authorization_list[0]; let actual = &transaction.authorization_list()[0]; @@ -213,6 +217,7 @@ fn evm_approvals_are_not_replaced_by_finalized_approvals() { fn evm_transaction_sign_replaces_approval_and_recomputes_identity() { let mut transaction = CasperTransaction::from(decode(signed_legacy_transaction().raw_rlp)); let old_hash = transaction.hash(); + let old_initiator = transaction.initiator_addr(); let new_secret_key = secp_secret_key([1; SecretKey::SECP256K1_LENGTH]); let expected_signer = PublicKey::from(&new_secret_key); @@ -226,6 +231,8 @@ fn evm_transaction_sign_replaces_approval_and_recomputes_identity() { &expected_signer ); assert_ne!(TransactionHash::from(evm_transaction.hash()), old_hash); + assert_ne!(evm_transaction.initiator_addr(), old_initiator); + assert_derived_initiator(&evm_transaction); evm_transaction .verify() .expect("signed transaction should verify"); @@ -349,6 +356,15 @@ where assert_eq!(&decoded, value); } +fn assert_derived_initiator(transaction: &EvmTransaction) { + let address = transaction.from(); + let initiator = transaction.initiator_addr(); + + assert_eq!(initiator, InitiatorAddr::Eoa(address)); + assert_eq!(initiator.account_hash(), None); + assert_eq!(initiator.evm_address(), Some(address)); +} + fn signed_legacy_transaction() -> SignedTransaction { let tx = TxLegacy { chain_id: Some(7),