diff --git a/AGENTS.md b/AGENTS.md index 5157298c44..7e8d056ded 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,4 +6,5 @@ - If EVM test fixtures under `target/evm-contracts/*.bin` are missing, run `make build-contracts-evm`. - If Wasm contract fixtures are missing, run `make build-contracts-rs`. - Keep `resources/local/chainspec.toml.in` in sync when editing chainspecs; run `./generate-chainspec.sh` when `resources/local/chainspec.toml` is missing or stale. +- Always prefer invoking the appropriate system contract over manually modifying mint-owned data. - Treat idempotent system contract/predeploy upserts in protocol upgrade handlers as standard activation behavior, not as an alternative to `global_state_update`. diff --git a/Cargo.lock b/Cargo.lock index 13e1669bd8..8d76379060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1710,6 +1710,7 @@ dependencies = [ "num_cpus", "once_cell", "openssl", + "parking_lot", "pin-project", "pnet", "pretty_assertions", diff --git a/node/Cargo.toml b/node/Cargo.toml index 9733087807..f947647f64 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -98,6 +98,7 @@ casper-executor-wasm = { version = "0.1.3", path = "../executor/wasm" } casper-executor-wasm-interface = { version = "0.1.3", path = "../executor/wasm_interface" } fs_extra = "1.3.0" casper-executor-evm = { version = "0.1.0", path = "../executor/evm" } +parking_lot = "0.12.5" [dev-dependencies] casper-binary-port = { version = "1.1.1", path = "../binary_port", features = ["testing"] } diff --git a/node/src/components/contract_runtime/error.rs b/node/src/components/contract_runtime/error.rs index 09af7a8736..672c9fea07 100644 --- a/node/src/components/contract_runtime/error.rs +++ b/node/src/components/contract_runtime/error.rs @@ -170,6 +170,9 @@ pub enum BlockExecutionError { UnsupportedTransactionKind(u8), #[error("Error while converting transaction to internal representation: {0}")] TransactionConversion(String), + /// Failed to reconcile EVM balance rounding with total supply. + #[error("Failed to reconcile EVM dust with total supply: {0}")] + EvmDust(String), /// Invalid gas limit amount. #[error("Invalid gas limit amount: {0}")] InvalidGasLimit(U512), diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 491a53251c..d2f31da58b 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -2,7 +2,10 @@ pub(crate) mod wasm_v2_request; use casper_executor_wasm::ExecutorV2; use itertools::Itertools; -use std::{collections::BTreeMap, convert::TryInto, sync::Arc, time::Instant}; +use parking_lot::RwLock; +use std::{ + cell::RefCell, collections::BTreeMap, convert::TryInto, rc::Rc, sync::Arc, time::Instant, +}; use tracing::{debug, error, info, trace, warn}; use wasm_v2_request::{WasmV2Request, WasmV2Result}; @@ -32,9 +35,12 @@ use casper_storage::{ lmdb::LmdbGlobalState, scratch::ScratchGlobalState, CommitProvider, ScratchProvider, StateProvider, StateReader, }, - system::runtime_native::Config as NativeRuntimeConfig, + system::{ + mint::Mint, + runtime_native::{Config as NativeRuntimeConfig, Id as NativeRuntimeId, RuntimeNative}, + }, tracking_copy::{TrackingCopyEntityExt, TrackingCopyError}, - TrackingCopy, + AddressGenerator, TrackingCopy, }; use casper_types::{ account::{Account, AccountHash}, @@ -45,10 +51,10 @@ use casper_types::{ ReceiptStatus as EvmReceiptStatus, }, execution::{Effects, ExecutionResult, TransformKindV2, TransformV2}, - system::handle_payment::ARG_AMOUNT, + system::{handle_payment::ARG_AMOUNT, MINT}, BlockHash, BlockHeader, BlockTime, BlockV2, CLValue, Chainspec, ChecksumRegistry, Digest, EntityAddr, EraEndV2, EraId, FeeHandling, Gas, InvalidTransaction, InvalidTransactionV1, Key, - ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, Transaction, + Phase, ProtocolVersion, PublicKey, RefundHandling, StoredValue, TimeDiff, Transaction, TransactionEntryPoint, AUCTION_LANE_ID, MINT_LANE_ID, U512, }; @@ -1203,7 +1209,35 @@ pub fn execute_finalized_block( .map_err(|error| { BlockExecutionError::TransactionConversion(error.to_string()) })?; - let execution_effects = tracking_copy.effects(); + // EVM balances were already rounded down by the executor. Call mint + // without debiting a purse, using the same tracking copy so its supply + // reduction commits with the EVM balance writes. + // TODO: Move this mint runtime setup out of block execution once the + // runtime refactor is complete. + let execution_effects = if outcome.dust_motes.is_zero() { + tracking_copy.effects() + } else { + let tracking_copy = Rc::new(RefCell::new(tracking_copy)); + let id = NativeRuntimeId::Transaction(transaction_hash); + let phase = Phase::Session; + let address_generator = + Arc::new(RwLock::new(AddressGenerator::new(&id.seed(), phase))); + let mut runtime = RuntimeNative::new_system_contract_runtime( + native_runtime_config.clone(), + protocol_version, + id, + address_generator, + Rc::clone(&tracking_copy), + phase, + MINT, + ) + .map_err(|error| BlockExecutionError::EvmDust(error.to_string()))?; + runtime + .reduce_total_supply(outcome.dust_motes) + .map_err(|error| BlockExecutionError::EvmDust(error.to_string()))?; + let effects = tracking_copy.borrow().effects(); + effects + }; state_root_hash = scratch_state.commit_effects(state_root_hash, execution_effects.clone())?; let effective_gas_price = evm_transaction.effective_gas_price(base_fee_wei); diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index ecf28a86cf..e46cdfdcb2 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -935,6 +935,32 @@ fn evm_coinbase_transfer_init_code() -> Vec { ]) } +fn evm_one_wei_transfer_init_code(recipient: evm::Address) -> Vec { + let mut runtime = vec![ + opcode::PUSH1, + 0, // return size + opcode::PUSH1, + 0, // return offset + opcode::PUSH1, + 0, // calldata size + opcode::PUSH1, + 0, // calldata offset + opcode::PUSH1, + 1, // value in wei + opcode::PUSH20, + ]; + runtime.extend_from_slice(recipient.as_bytes()); + runtime.extend_from_slice(&[ + opcode::PUSH2, + 0xff, + 0xff, // gas + opcode::CALL, + opcode::POP, + opcode::STOP, + ]); + evm_init_code_returning(runtime) +} + fn signed_evm_deploy_transaction(chain_id: u64) -> EvmTransaction { signed_evm_create_transaction(chain_id, 0, evm_log_emitting_init_code()) } @@ -1318,6 +1344,79 @@ async fn should_execute_evm_transaction_and_store_receipt() { ); } +#[tokio::test] +async fn should_reduce_total_supply_for_evm_rounding_dust_without_debiting_sender_again() { + let evm_config = EvmConfig { + enabled: true, + chain_id: 0x4353_50FF, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 1, + 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::PayToProposer); + 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 recipient = evm::Address::new([0x56; 20]); + let deploy = signed_evm_create_transaction( + evm_config.chain_id, + 0, + evm_one_wei_transfer_init_code(recipient), + ); + let sender = deploy.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + let (_hash, deploy_height, deploy_result) = + test.send_transaction(Transaction::from(deploy)).await; + let ExecutionResult::Evm(deploy_result) = deploy_result else { + panic!("expected EVM deploy execution result"); + }; + let contract = deploy_result + .receipt + .contract_address + .expect("EVM deployment should create contract"); + + let sender_balance_before = evm_balance(&mut test.fixture, sender, deploy_height); + let total_supply_before = test.get_total_supply(Some(deploy_height)); + let call = signed_evm_call_transaction(evm_config.chain_id, 1, contract, 1, Vec::new()); + let fee = call + .max_fee_amount(&evm_config) + .expect("maximum EVM fee should fit"); + let (_hash, call_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); + assert_eq!( + evm_balance(&mut test.fixture, sender, call_height), + sender_balance_before - fee - U512::one(), + ); + assert_eq!( + evm_balance(&mut test.fixture, contract, call_height), + U512::zero() + ); + assert_eq!( + evm_balance(&mut test.fixture, recipient, call_height), + U512::zero() + ); + assert_eq!( + test.get_total_supply(Some(call_height)), + total_supply_before - U512::one(), + ); +} + #[tokio::test] async fn should_prelink_ed25519_proposer_coinbase_for_evm_execution() { let evm_config = EvmConfig {