diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 8f97eb2041..d98bdd7184 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -7,7 +7,7 @@ use alloy_eips::{ Authorization as AlloyAuthorization, SignedAuthorization as AlloySignedAuthorization, }, }; -use alloy_primitives::{Address as AlloyAddress, Signature, TxKind, B256, U256}; +use alloy_primitives::{keccak256, Address as AlloyAddress, Signature, TxKind, B256, U256}; use casper_executor_evm::{ BlockContext, BlockHashProvider, BlockHashProviderResult, CallRequest, CallValidation, Error, EvmExecutor, ExecuteKind, ExecuteRequest, ExecutionStatus, EMPTY_CODE_HASH, @@ -24,10 +24,11 @@ use casper_storage::{ use casper_types::{ bytesrepr::{FromBytes, ToBytes}, contracts::NamedKeys, - evm, AccessRights, Account, BlockHash, CLValue, ChainspecRegistry, Digest, EvmAddr, EvmConfig, - EvmSpec, EvmTransaction, GenesisAccount, GenesisConfig, HoldBalanceHandling, Key, Motes, - ProtocolVersion, PublicKey, SecretKey, StorageCosts, StoredValue, SystemConfig, Timestamp, - URef, WasmConfig, DEFAULT_WEI_PER_MOTE, U256 as CasperU256, U512, + evm, AccessRights, Account, BlockHash, ByteCode, ByteCodeKind, CLValue, ChainspecRegistry, + Digest, EvmAddr, EvmConfig, EvmSpec, EvmTransaction, GenesisAccount, GenesisConfig, + HoldBalanceHandling, Key, Motes, ProtocolVersion, PublicKey, SecretKey, StorageCosts, + StoredValue, SystemConfig, Timestamp, URef, WasmConfig, DEFAULT_WEI_PER_MOTE, + U256 as CasperU256, U512, }; use revm::bytecode::opcode; @@ -187,6 +188,46 @@ fn reverting_contract_init_code() -> Vec { init_code_returning(runtime) } +fn reverting_runtime() -> Vec { + vec![opcode::PUSH1, 0, opcode::PUSH1, 0, opcode::REVERT] +} + +fn coinbase_transfer_init_code() -> Vec { + let revert_offset = 19u8; + let runtime = vec![ + opcode::PUSH1, + 0, // return size + opcode::PUSH1, + 0, // return offset + opcode::PUSH1, + 0, // calldata size + opcode::PUSH1, + 0, // calldata offset + opcode::CALLVALUE, + opcode::COINBASE, + opcode::PUSH2, + 0x08, + 0xfc, // 2300 gas + opcode::CALL, + opcode::ISZERO, + opcode::PUSH1, + revert_offset, + opcode::JUMPI, + opcode::STOP, + opcode::JUMPDEST, + opcode::PUSH1, + 0, + opcode::PUSH1, + 0, + opcode::REVERT, + ]; + init_code_returning(runtime) +} + +fn coinbase_observer_init_code() -> Vec { + init_code_returning(vec![opcode::COINBASE, opcode::POP, opcode::STOP]) +} + fn call_request( from: evm::Address, to: Option, @@ -521,6 +562,42 @@ fn read_balance>( } } +fn read_account_balance>( + tracking_copy: &mut TrackingCopy, + account_hash: casper_types::account::AccountHash, +) -> U512 { + let main_purse = match tracking_copy + .read(&Key::Account(account_hash)) + .expect("account read should not fail") + { + Some(StoredValue::Account(account)) => account.main_purse(), + Some(other) => panic!("unexpected account value: {other:?}"), + None => return U512::zero(), + }; + match tracking_copy + .read(&Key::Balance(main_purse.addr())) + .expect("balance read should not fail") + { + Some(StoredValue::CLValue(value)) => value.into_t::().unwrap(), + Some(other) => panic!("unexpected balance value: {other:?}"), + None => U512::zero(), + } +} + +fn read_evm_identity>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, +) -> Option { + match tracking_copy + .read(&Key::Evm(EvmAddr::Account(address))) + .expect("identity read should not fail") + { + Some(StoredValue::CLValue(value)) => Some(value.into_t::().unwrap()), + Some(other) => panic!("unexpected EVM identity value: {other:?}"), + None => None, + } +} + fn seed_evm_balance>( tracking_copy: &mut TrackingCopy, address: evm::Address, @@ -545,6 +622,41 @@ fn seed_evm_balance>( ); } +fn seed_account>( + tracking_copy: &mut TrackingCopy, + account_hash: casper_types::account::AccountHash, + main_purse: URef, + balance: U512, +) { + tracking_copy.write( + Key::Account(account_hash), + StoredValue::Account(Account::create(account_hash, NamedKeys::new(), main_purse)), + ); + tracking_copy.write( + Key::Balance(main_purse.addr()), + StoredValue::CLValue(CLValue::from_t(balance).unwrap()), + ); +} + +fn seed_evm_code>( + tracking_copy: &mut TrackingCopy, + address: evm::Address, + code: Vec, +) { + let digest = keccak256(&code); + let mut hash = [0u8; evm::HASH_LENGTH]; + hash.copy_from_slice(digest.as_slice()); + let code_hash = evm::Hash::new(hash); + tracking_copy.write( + Key::Evm(EvmAddr::CodeHash(address)), + StoredValue::CLValue(CLValue::from_t(code_hash).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::ByteCode(code_hash)), + StoredValue::ByteCode(ByteCode::new(ByteCodeKind::EvmPrague, code)), + ); +} + fn read_evm_nonce>( tracking_copy: &mut TrackingCopy, address: evm::Address, @@ -924,17 +1036,335 @@ fn erc20_and_native_purse_balances_update() { assert_eq!(decode_word(&remaining_allowance.output), 60); } +#[test] +fn coinbase_transfer_to_prelinked_beneficiary_credits_proposer_account() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([42; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([8; 32], AccessRights::READ_ADD_WRITE); + let proposer_initial_balance = U512::from(1_000u64); + let transfer_value = CasperU256::from(250u64); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + proposer_initial_balance, + ); + tracking_copy.write( + Key::Evm(EvmAddr::Account(beneficiary)), + StoredValue::CLValue(CLValue::from_t(Key::Account(proposer_account_hash)).unwrap()), + ); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_transfer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), transfer_value); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase transfer should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::Account(proposer_account_hash)) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + proposer_initial_balance + U512::from(transfer_value) + ); +} + +#[test] +fn coinbase_transfer_without_prelink_uses_evm_native_identity() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([43; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([9; 32], AccessRights::READ_ADD_WRITE); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + U512::zero(), + ); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_transfer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), CasperU256::from(250u64)); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase transfer should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert!(matches!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::URef(_)) + )); + assert_eq!( + read_balance(&mut tracking_copy, beneficiary), + U512::from(250u64) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + U512::zero() + ); +} + +#[test] +fn reading_coinbase_without_credit_creates_only_evm_native_identity() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([43; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_observer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), CasperU256::zero()); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase observer should execute"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert!(matches!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::URef(_)) + )); +} + +#[test] +fn coinbase_transfer_to_linked_beneficiary_with_code_executes_code() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([44; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([10; 32], AccessRights::READ_ADD_WRITE); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + U512::zero(), + ); + tracking_copy.write( + Key::Evm(EvmAddr::Account(beneficiary)), + StoredValue::CLValue(CLValue::from_t(Key::Account(proposer_account_hash)).unwrap()), + ); + seed_evm_code(&mut tracking_copy, beneficiary, reverting_runtime()); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_transfer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), CasperU256::from(250u64)); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase transfer should execute EVM code"); + + assert_eq!(outcome.status, ExecutionStatus::Revert); + assert_eq!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::Account(proposer_account_hash)) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + U512::zero() + ); +} + +#[test] +fn coinbase_transfer_keeps_existing_evm_native_beneficiary_identity() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([46; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([12; 32], AccessRights::READ_ADD_WRITE); + let existing_purse = URef::new([13; 32], AccessRights::READ_ADD_WRITE); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + U512::zero(), + ); + tracking_copy.write( + Key::Evm(EvmAddr::Account(beneficiary)), + StoredValue::CLValue(CLValue::from_t(Key::URef(existing_purse)).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::Nonce(beneficiary)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::CodeHash(beneficiary)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_transfer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), CasperU256::from(250u64)); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase transfer should preserve existing identity"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::URef(existing_purse)) + ); + assert_eq!( + read_balance(&mut tracking_copy, beneficiary), + U512::from(250u64) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + U512::zero() + ); +} + +#[test] +fn coinbase_transfer_keeps_existing_account_beneficiary_identity() { + let executor = executor(EvmSpec::Prague); + let sender = evm::Address::new([1; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([47; SecretKey::ED25519_LENGTH]).unwrap(); + let existing_secret_key = + SecretKey::ed25519_from_bytes([48; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let existing_account = PublicKey::from(&existing_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let existing_account_hash = existing_account.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([14; 32], AccessRights::READ_ADD_WRITE); + let existing_main_purse = URef::new([15; 32], AccessRights::READ_ADD_WRITE); + let existing_initial_balance = U512::from(500u64); + let transfer_value = CasperU256::from(250u64); + let (mut tracking_copy, _tempdir) = tracking_copy(); + + seed_evm_balance(&mut tracking_copy, sender, U512::from(10_000_000u64)); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + U512::zero(), + ); + seed_account( + &mut tracking_copy, + existing_account_hash, + existing_main_purse, + existing_initial_balance, + ); + tracking_copy.write( + Key::Evm(EvmAddr::Account(beneficiary)), + StoredValue::CLValue(CLValue::from_t(Key::Account(existing_account_hash)).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::Nonce(beneficiary)), + StoredValue::CLValue(CLValue::from_t(0u64).unwrap()), + ); + tracking_copy.write( + Key::Evm(EvmAddr::CodeHash(beneficiary)), + StoredValue::CLValue(CLValue::from_t(EMPTY_CODE_HASH).unwrap()), + ); + let contract = deploy_code( + &executor, + &mut tracking_copy, + sender, + coinbase_transfer_init_code(), + ); + let mut request = call_request(sender, Some(contract), Vec::new(), transfer_value); + request.block.beneficiary = beneficiary; + + let outcome = executor + .execute(&mut tracking_copy, request) + .expect("coinbase transfer should preserve existing account identity"); + + assert_eq!(outcome.status, ExecutionStatus::Success); + assert_eq!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::Account(existing_account_hash)) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, existing_account_hash), + existing_initial_balance + U512::from(transfer_value) + ); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + U512::zero() + ); +} + #[test] fn nonzero_gas_price_does_not_charge_evm_balances() { let executor = executor(EvmSpec::Prague); let sender = evm::Address::new([1; 20]); let recipient = evm::Address::new([2; 20]); - let beneficiary = evm::Address::new([3; 20]); + let proposer_secret_key = + SecretKey::ed25519_from_bytes([45; SecretKey::ED25519_LENGTH]).unwrap(); + let proposer = PublicKey::from(&proposer_secret_key); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let proposer_main_purse = URef::new([11; 32], AccessRights::READ_ADD_WRITE); let (mut tracking_copy, _tempdir) = tracking_copy(); let initial_balance = U512::from(10_000_000u64); let transfer_value = CasperU256::from(250u64); seed_evm_balance(&mut tracking_copy, sender, initial_balance); + seed_account( + &mut tracking_copy, + proposer_account_hash, + proposer_main_purse, + U512::zero(), + ); let mut block_context = block(); block_context.beneficiary = beneficiary; let request = ExecuteRequest { @@ -964,7 +1394,15 @@ fn nonzero_gas_price_does_not_charge_evm_balances() { read_balance(&mut tracking_copy, recipient), U512::from(250u64) ); + assert!(matches!( + read_evm_identity(&mut tracking_copy, beneficiary), + Some(Key::URef(_)) + )); assert_eq!(read_balance(&mut tracking_copy, beneficiary), U512::zero()); + assert_eq!( + read_account_balance(&mut tracking_copy, proposer_account_hash), + U512::zero() + ); } #[test] diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 6b087fa962..f1f70fed5e 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -392,6 +392,32 @@ where } } +fn evm_account_has_nonce( + tracking_copy: &mut TrackingCopy, + address: EvmAddress, +) -> Result +where + R: StateReader, +{ + let key = Key::Evm(casper_types::EvmAddr::Nonce(address)); + match tracking_copy + .read(&key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let _nonce = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + Ok(true) + } + Some(stored_value) => Err(BlockExecutionError::PaymentError(format!( + "unexpected stored value for {key}: expected StoredValue::CLValue(u64), found {}", + stored_value.type_name() + ))), + None => Ok(false), + } +} + fn account_main_purse( tracking_copy: &mut TrackingCopy, protocol_version: ProtocolVersion, @@ -413,6 +439,51 @@ where } } +fn apply_evm_proposer_identity( + tracking_copy: &mut TrackingCopy, + protocol_version: ProtocolVersion, + proposer: &PublicKey, +) -> Result<(), BlockExecutionError> +where + R: StateReader, +{ + let address = EvmAddress::from_block_proposer_public_key(proposer); + let account_hash = proposer.to_account_hash(); + if account_main_purse(tracking_copy, protocol_version, account_hash)?.is_none() { + return Ok(()); + } + + let identity_key = Key::Evm(casper_types::EvmAddr::Account(address)); + match tracking_copy + .read(&identity_key) + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))? + { + Some(StoredValue::CLValue(cl_value)) => { + let identity = cl_value + .into_t::() + .map_err(|error| BlockExecutionError::PaymentError(error.to_string()))?; + match identity { + Key::Account(_) | Key::URef(_) => Ok(()), + other => Err(BlockExecutionError::PaymentError(format!( + "invalid EVM account identity key: {other}" + ))), + } + } + Some(stored_value) => Err(BlockExecutionError::PaymentError(format!( + "unexpected stored value for {identity_key}: expected StoredValue::CLValue(Key), found {}", + stored_value.type_name() + ))), + None => { + if evm_account_has_code(tracking_copy, address)? + || evm_account_has_nonce(tracking_copy, address)? + { + return Ok(()); + } + write_evm_identity(tracking_copy, address, Key::Account(account_hash)) + } + } +} + fn apply_evm_identity_plan( tracking_copy: &mut TrackingCopy, protocol_version: ProtocolVersion, @@ -1106,8 +1177,7 @@ pub fn execute_finalized_block( let block_context = EvmBlockContext { number: block_height, timestamp: block_time.value() / 1000, - beneficiary: EvmAddress::from_public_key(&proposer) - .unwrap_or(EvmAddress::ZERO), + beneficiary: EvmAddress::from_block_proposer_public_key(&proposer), gas_limit: Some(chainspec.evm_config.block_gas_limit), base_fee: Some(base_fee_wei), }; @@ -1130,6 +1200,7 @@ pub fn execute_finalized_block( identity_plan, )?; } + apply_evm_proposer_identity(&mut tracking_copy, protocol_version, &proposer)?; let outcome = EvmExecutor::new(chainspec.evm_config) .execute_with_block_hash_provider( &mut tracking_copy, diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index 94c16a91e4..0173c149ee 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -883,15 +883,90 @@ fn evm_log_emitting_init_code() -> Vec { init_code } +fn evm_init_code_returning(runtime: Vec) -> Vec { + let runtime_len = u8::try_from(runtime.len()).expect("runtime should fit in PUSH1"); + let runtime_offset = 12u8; + let mut init_code = vec![ + opcode::PUSH1, + runtime_len, + opcode::PUSH1, + runtime_offset, + opcode::PUSH1, + 0, + opcode::CODECOPY, + opcode::PUSH1, + runtime_len, + opcode::PUSH1, + 0, + opcode::RETURN, + ]; + init_code.extend(runtime); + init_code +} + +fn evm_coinbase_transfer_init_code() -> Vec { + let revert_offset = 19u8; + evm_init_code_returning(vec![ + opcode::PUSH1, + 0, + opcode::PUSH1, + 0, + opcode::PUSH1, + 0, + opcode::PUSH1, + 0, + opcode::CALLVALUE, + opcode::COINBASE, + opcode::PUSH2, + 0x08, + 0xfc, + opcode::CALL, + opcode::ISZERO, + opcode::PUSH1, + revert_offset, + opcode::JUMPI, + opcode::STOP, + opcode::JUMPDEST, + opcode::PUSH1, + 0, + opcode::PUSH1, + 0, + opcode::REVERT, + ]) +} + fn signed_evm_deploy_transaction(chain_id: u64) -> EvmTransaction { + signed_evm_create_transaction(chain_id, 0, evm_log_emitting_init_code()) +} + +fn signed_evm_create_transaction(chain_id: u64, nonce: u64, init_code: Vec) -> EvmTransaction { let transaction = TxLegacy { chain_id: Some(chain_id), - nonce: 0, + nonce, gas_price: EVM_TEST_GAS_PRICE, gas_limit: EVM_TEST_GAS_LIMIT, to: TxKind::Create, value: U256::ZERO, - input: AlloyBytes::from(evm_log_emitting_init_code()), + input: AlloyBytes::from(init_code), + }; + signed_evm_legacy_transaction(transaction) +} + +fn signed_evm_call_transaction( + chain_id: u64, + nonce: u64, + recipient: evm::Address, + value: u64, + input: Vec, +) -> EvmTransaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit: EVM_TEST_GAS_LIMIT, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: U256::from(value), + input: AlloyBytes::from(input), }; signed_evm_legacy_transaction(transaction) } @@ -1197,6 +1272,97 @@ async fn should_execute_evm_transaction_and_store_receipt() { ); } +#[tokio::test] +async fn should_prelink_ed25519_proposer_coinbase_for_evm_execution() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + wei_per_mote: DEFAULT_WEI_PER_MOTE, + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config) + .with_refund_handling(RefundHandling::NoRefund) + .with_fee_handling(FeeHandling::Burn); + let mut test = SingleTransactionTestCase::new( + Arc::clone(&ALICE_SECRET_KEY), + Arc::clone(&BOB_SECRET_KEY), + Arc::clone(&CHARLIE_SECRET_KEY), + Some(config), + ) + .await; + test.fixture + .run_until_consensus_in_era(ERA_ONE, ONE_MIN) + .await; + + let deploy = + signed_evm_create_transaction(evm_config.chain_id, 0, evm_coinbase_transfer_init_code()); + let sender = deploy.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + + let (_txn_hash, deploy_block_height, deploy_result) = + test.send_transaction(Transaction::from(deploy)).await; + let ExecutionResult::Evm(deploy_result) = deploy_result else { + panic!("expected EVM deploy execution result"); + }; + assert_eq!(deploy_result.receipt.status, evm::ReceiptStatus::Success); + let contract = deploy_result + .receipt + .contract_address + .expect("EVM deployment should create contract"); + + let deploy_block = test.fixture.get_block_by_height(deploy_block_height); + let deploy_proposer = deploy_block.proposer().clone(); + assert!( + evm::Address::from_public_key(&deploy_proposer).is_none(), + "test expects an ed25519 proposer" + ); + let deploy_proposer_account_hash = deploy_proposer.to_account_hash(); + let deploy_beneficiary = evm::Address::from_block_proposer_public_key(&deploy_proposer); + assert_eq!( + evm_identity_at(&mut test.fixture, deploy_block_height, deploy_beneficiary), + Key::Account(deploy_proposer_account_hash) + ); + + let transfer_value = 250u64; + let call = + signed_evm_call_transaction(evm_config.chain_id, 1, contract, transfer_value, Vec::new()); + let (_txn_hash, call_block_height, call_result) = + test.send_transaction(Transaction::from(call)).await; + let ExecutionResult::Evm(call_result) = call_result else { + panic!("expected EVM call execution result"); + }; + assert_eq!(call_result.receipt.status, evm::ReceiptStatus::Success); + + let payout_block = test.fixture.get_block_by_height(call_block_height); + let proposer = payout_block.proposer().clone(); + assert!( + evm::Address::from_public_key(&proposer).is_none(), + "test expects an ed25519 proposer" + ); + let proposer_account_hash = proposer.to_account_hash(); + let beneficiary = evm::Address::from_block_proposer_public_key(&proposer); + let mut expected_beneficiary = [0u8; evm::ADDRESS_LENGTH]; + expected_beneficiary.copy_from_slice(&proposer_account_hash.as_bytes()[12..]); + assert_eq!(beneficiary, evm::Address::new(expected_beneficiary)); + + assert_eq!( + evm_identity_at(&mut test.fixture, call_block_height, beneficiary), + Key::Account(proposer_account_hash) + ); + let proposer_before = get_balance(&test.fixture, &proposer, Some(deploy_block_height), true) + .total_balance() + .copied() + .expect("proposer should have balance before EVM payout"); + let proposer_after = get_balance(&test.fixture, &proposer, Some(call_block_height), true) + .total_balance() + .copied() + .expect("proposer should have balance after EVM payout"); + assert_eq!(proposer_after, proposer_before + U512::from(transfer_value)); +} + #[tokio::test] async fn should_apply_casper_refund_handling_to_evm_transaction() { let evm_config = EvmConfig { diff --git a/types/src/evm/address.rs b/types/src/evm/address.rs index a0d93750a4..bd655289a2 100644 --- a/types/src/evm/address.rs +++ b/types/src/evm/address.rs @@ -69,6 +69,26 @@ impl Address { address.copy_from_slice(&digest.as_slice()[digest.len() - ADDRESS_LENGTH..]); Some(Address::new(address)) } + + /// Returns the EVM `block.coinbase` receive address for a Casper block proposer. + /// + /// Secp256k1 proposers use their Ethereum-native address. Non-secp256k1 + /// proposers use a Casper-defined alias derived from the last 20 bytes of + /// their account hash. That alias is only a receive address for proposer + /// rewards and `block.coinbase` transfers; it is not controlled by an + /// Ethereum signing key and cannot be used to sign EVM transactions. + pub fn from_block_proposer_public_key(public_key: &PublicKey) -> Self { + if let Some(address) = Self::from_public_key(public_key) { + return address; + } + + let account_hash = public_key.to_account_hash(); + let mut address = [0u8; ADDRESS_LENGTH]; + address.copy_from_slice( + &account_hash.as_bytes()[account_hash.as_bytes().len() - ADDRESS_LENGTH..], + ); + Address::new(address) + } } impl AsRef<[u8]> for Address { @@ -134,7 +154,7 @@ impl FromBytes for Address { #[cfg(test)] mod tests { use super::*; - use crate::CLValue; + use crate::{CLValue, SecretKey}; #[test] fn evm_address_cl_value_roundtrip() { @@ -152,4 +172,40 @@ mod tests { address ); } + + #[test] + fn block_proposer_address_preserves_secp256k1_ethereum_address() { + let secret_key = SecretKey::secp256k1_from_bytes([7; SecretKey::SECP256K1_LENGTH]).unwrap(); + let public_key = PublicKey::from(&secret_key); + + assert_eq!( + Address::from_block_proposer_public_key(&public_key), + Address::from_public_key(&public_key).unwrap() + ); + } + + #[test] + fn block_proposer_address_uses_last_twenty_account_hash_bytes_for_ed25519() { + let secret_key = SecretKey::ed25519_from_bytes([9; SecretKey::ED25519_LENGTH]).unwrap(); + let public_key = PublicKey::from(&secret_key); + let account_hash = public_key.to_account_hash(); + let mut expected = [0u8; ADDRESS_LENGTH]; + expected.copy_from_slice(&account_hash.as_bytes()[12..]); + + assert_eq!( + Address::from_block_proposer_public_key(&public_key), + Address::new(expected) + ); + assert!(Address::from_public_key(&public_key).is_none()); + } + + #[test] + fn block_proposer_address_is_total_for_system_public_key() { + let address = Address::from_block_proposer_public_key(&PublicKey::System); + let account_hash = PublicKey::System.to_account_hash(); + let mut expected = [0u8; ADDRESS_LENGTH]; + expected.copy_from_slice(&account_hash.as_bytes()[12..]); + + assert_eq!(address, Address::new(expected)); + } }