diff --git a/executor/evm/tests/executor.rs b/executor/evm/tests/executor.rs index 906efc77af..92a0c19ba4 100644 --- a/executor/evm/tests/executor.rs +++ b/executor/evm/tests/executor.rs @@ -116,6 +116,7 @@ fn executor(spec: EvmSpec) -> EvmExecutor { block_gas_limit: 30_000_000, base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), }) } @@ -1465,6 +1466,7 @@ fn whole_mote_value_executes_in_wei_and_persists_without_dust() { block_gas_limit: 30_000_000, base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), }); let sender = evm::Address::new([0x31; 20]); let recipient = evm::Address::new([0x32; 20]); @@ -2174,6 +2176,7 @@ fn unchecked_call_with_calldata_does_not_underflow_unfunded_sender() { block_gas_limit: 30_000_000, base_fee: 1_000_000, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), }; let gas_price = evm_config.base_fee_wei(); let executor = EvmExecutor::new(evm_config); @@ -2359,6 +2362,7 @@ fn signed_transactions_require_configured_chain_id() { block_gas_limit: 30_000_000, base_fee: 0, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), }); let transaction = legacy_transaction(Some(7)); let request = ExecuteRequest { diff --git a/node/src/components/block_accumulator/tests.rs b/node/src/components/block_accumulator/tests.rs index 714c349f67..1c149e6ecb 100644 --- a/node/src/components/block_accumulator/tests.rs +++ b/node/src/components/block_accumulator/tests.rs @@ -16,8 +16,8 @@ use tokio::time; use casper_types::{ generate_ed25519_keypair, testing::TestRng, ActivationPoint, BlockV2, ChainNameDigest, - Chainspec, ChainspecRawBytes, FinalitySignature, FinalitySignatureV2, ProtocolVersion, - PublicKey, SecretKey, Signature, TestBlockBuilder, TransactionConfig, U512, + Chainspec, ChainspecRawBytes, EvmConfig, FinalitySignature, FinalitySignatureV2, + ProtocolVersion, PublicKey, SecretKey, Signature, TestBlockBuilder, TransactionConfig, U512, }; use reactor::ReactorEvent; @@ -208,6 +208,7 @@ impl Reactor for MockReactor { Some(registry), false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); storage.initialize_for_test(); diff --git a/node/src/components/block_validator/state.rs b/node/src/components/block_validator/state.rs index 51ade2ecf0..50a60d7e4c 100644 --- a/node/src/components/block_validator/state.rs +++ b/node/src/components/block_validator/state.rs @@ -8,8 +8,8 @@ use datasize::DataSize; use tracing::{debug, error, warn}; use casper_types::{ - Approval, ApprovalsHash, Chainspec, FinalitySignatureId, Timestamp, TransactionConfig, - TransactionHash, + Approval, ApprovalsHash, Chainspec, EvmConfig, FinalitySignatureId, Timestamp, + TransactionConfig, TransactionHash, }; use crate::{ @@ -136,9 +136,11 @@ impl BlockValidationState { // this is an optimization, rejects proposal that exceeds lane limits OR // proposes a transaction in an unsupported lane - if let Err(err) = - Self::validate_transaction_lane_counts(proposed_block, &chainspec.transaction_config) - { + if let Err(err) = Self::validate_transaction_lane_counts( + proposed_block, + &chainspec.transaction_config, + &chainspec.evm_config, + ) { let state = BlockValidationState::Invalid { timestamp: proposed_block.timestamp(), error: err, @@ -196,6 +198,7 @@ impl BlockValidationState { let state = BlockValidationState::InProgress { appendable_block: AppendableBlock::new( chainspec.transaction_config.clone(), + chainspec.evm_config.clone(), current_gas_price, proposed_block.timestamp(), ), @@ -211,16 +214,22 @@ impl BlockValidationState { fn validate_transaction_lane_counts( block: &ProposedBlock, config: &TransactionConfig, + evm_config: &EvmConfig, ) -> Result<(), Box> { - let lanes = config.transaction_v1_config.get_supported_lanes(); + let mut lanes = config.transaction_v1_config.get_supported_lanes(); + lanes.extend(evm_config.get_supported_lanes()); if block.value().has_transaction_in_unsupported_lane(&lanes) { return Err(Box::new(InvalidProposalError::UnsupportedLane)); } for supported_lane in lanes { let transactions = block.value().count(Some(supported_lane)); - let lane_count_limit = config - .transaction_v1_config - .get_max_transaction_count(supported_lane); + let lane_count_limit = if evm_config.is_supported(supported_lane) { + evm_config.get_max_transaction_count(supported_lane) + } else { + config + .transaction_v1_config + .get_max_transaction_count(supported_lane) + }; if lane_count_limit < transactions as u64 { warn!( supported_lane, diff --git a/node/src/components/contract_runtime/operations.rs b/node/src/components/contract_runtime/operations.rs index 491a53251c..e78dae801f 100644 --- a/node/src/components/contract_runtime/operations.rs +++ b/node/src/components/contract_runtime/operations.rs @@ -686,6 +686,7 @@ pub fn execute_finalized_block( &stored_transaction, chainspec.core_config.pricing_handling, transaction_config, + &chainspec.evm_config, ) .map_err(|err| BlockExecutionError::TransactionConversion(err.to_string()))?; @@ -745,7 +746,7 @@ pub fn execute_finalized_block( // chainspec's refund and fee handling to the unused amount. let cost = if let Some(evm_transaction) = evm_transaction { evm_transaction - .max_fee_amount(&chainspec.evm_config) + .max_fee_amount(&chainspec.evm_config.fee_config()) .ok_or_else(|| { BlockExecutionError::PaymentError( "EVM fee amount overflowed U512".to_string(), @@ -1028,7 +1029,7 @@ pub fn execute_finalized_block( let actual_cost = artifact_builder.actual_cost(); // use actual cost here let required_balance = if let Some(evm_transaction) = evm_transaction { evm_transaction - .required_balance(actual_cost, &chainspec.evm_config) + .required_balance(actual_cost, &chainspec.evm_config.fee_config()) .ok_or_else(|| { BlockExecutionError::PaymentError( "EVM value is not an exact mote amount or value plus fee overflowed U512" @@ -1198,7 +1199,7 @@ pub fn execute_finalized_block( )?; } apply_evm_proposer_identity(&mut tracking_copy, protocol_version, &proposer)?; - let outcome = EvmExecutor::new(chainspec.evm_config) + let outcome = EvmExecutor::new(chainspec.evm_config.clone()) .execute(data_access_layer, &mut tracking_copy, request) .map_err(|error| { BlockExecutionError::TransactionConversion(error.to_string()) @@ -1213,7 +1214,7 @@ pub fn execute_finalized_block( evm_transaction.gas_limit(), ); let consumed = evm_transaction - .fee_amount(consumed_gas, &chainspec.evm_config) + .fee_amount(consumed_gas, &chainspec.evm_config.fee_config()) .ok_or_else(|| { BlockExecutionError::PaymentError( "EVM fee amount overflowed U512".to_string(), @@ -1982,6 +1983,7 @@ where &input_transaction, chainspec.core_config.pricing_handling, transaction_config, + &chainspec.evm_config, ); if let Err(error) = maybe_transaction { return SpeculativeExecutionResult::invalid_transaction(error); @@ -2210,7 +2212,7 @@ where block: block_context, kind, }; - let outcome = match EvmExecutor::new(chainspec.evm_config).execute( + let outcome = match EvmExecutor::new(chainspec.evm_config.clone()).execute( data_access_layer, &mut tracking_copy, execute_request, @@ -2426,6 +2428,7 @@ mod tests { block_gas_limit: 30_000_000, base_fee: 3, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), }, ..Default::default() }; diff --git a/node/src/components/contract_runtime/tests.rs b/node/src/components/contract_runtime/tests.rs index cc72fdc588..5d5796b58f 100644 --- a/node/src/components/contract_runtime/tests.rs +++ b/node/src/components/contract_runtime/tests.rs @@ -13,7 +13,7 @@ use tempfile::TempDir; use casper_types::{ bytesrepr::Bytes, contracts::ProtocolVersionMajor, evm, runtime_args, BlockHash, BlockHeader, - Chainspec, ChainspecRawBytes, Deploy, Digest, EntityVersion, EraId, EvmTransaction, + Chainspec, ChainspecRawBytes, Deploy, Digest, EntityVersion, EraId, EvmConfig, EvmTransaction, ExecutableDeployItem, PackageHash, PricingMode, PublicKey, RuntimeArgs, SecretKey, TestBlockBuilder, TimeDiff, Timestamp, Transaction, TransactionConfig, TransactionRuntimeParams, MINT_LANE_ID, U256, U512, @@ -149,6 +149,7 @@ impl reactor::Reactor for Reactor { Some(registry), false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); storage.initialize_for_test(); diff --git a/node/src/components/fetcher/tests.rs b/node/src/components/fetcher/tests.rs index 9b2a230e2f..ca7a27a820 100644 --- a/node/src/components/fetcher/tests.rs +++ b/node/src/components/fetcher/tests.rs @@ -12,8 +12,8 @@ use tempfile::TempDir; use thiserror::Error; use casper_types::{ - testing::TestRng, BlockV2, Chainspec, ChainspecRawBytes, FinalitySignatureV2, Transaction, - TransactionConfig, TransactionHash, TransactionId, + testing::TestRng, BlockV2, Chainspec, ChainspecRawBytes, EvmConfig, FinalitySignatureV2, + Transaction, TransactionConfig, TransactionHash, TransactionId, }; use super::*; @@ -304,6 +304,7 @@ impl ReactorTrait for Reactor { Some(registry), false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); storage.initialize_for_test(); diff --git a/node/src/components/gossiper/tests.rs b/node/src/components/gossiper/tests.rs index 54744fd29c..751d7c6df4 100644 --- a/node/src/components/gossiper/tests.rs +++ b/node/src/components/gossiper/tests.rs @@ -18,7 +18,7 @@ use tokio::time; use tracing::debug; use casper_types::{ - testing::TestRng, BlockV2, Chainspec, ChainspecRawBytes, EraId, FinalitySignatureV2, + testing::TestRng, BlockV2, Chainspec, ChainspecRawBytes, EraId, EvmConfig, FinalitySignatureV2, ProtocolVersion, TimeDiff, Transaction, TransactionConfig, }; @@ -180,6 +180,7 @@ impl reactor::Reactor for Reactor { Some(registry), false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); storage.initialize_for_test(); diff --git a/node/src/components/storage.rs b/node/src/components/storage.rs index 4f8bce1acc..b420cb8181 100644 --- a/node/src/components/storage.rs +++ b/node/src/components/storage.rs @@ -67,8 +67,9 @@ use casper_types::{ execution::{execution_result_v1, ExecutionResult, ExecutionResultV1}, Approval, ApprovalsHash, AvailableBlockRange, Block, BlockBody, BlockHash, BlockHeader, BlockHeaderWithSignatures, BlockSignatures, BlockSignaturesV1, BlockSignaturesV2, BlockV2, - ChainNameDigest, DeployHash, Digest, EraId, ExecutionInfo, FinalitySignature, ProtocolVersion, - Timestamp, Transaction, TransactionConfig, TransactionHash, TransactionId, Transfer, U512, + ChainNameDigest, DeployHash, Digest, EraId, EvmConfig, ExecutionInfo, FinalitySignature, + ProtocolVersion, Timestamp, Transaction, TransactionConfig, TransactionHash, TransactionId, + Transfer, U512, }; use datasize::DataSize; use num_rational::Ratio; @@ -150,6 +151,8 @@ pub struct Storage { chain_name_hash: ChainNameDigest, /// The transaction config as specified by the chainspec. transaction_config: TransactionConfig, + /// The EVM config as specified by the chainspec. + evm_config: EvmConfig, /// The utilization of blocks. utilization_tracker: BTreeMap>, /// Component initialization state. @@ -446,6 +449,7 @@ impl Storage { registry: Option<&Registry>, force_resync: bool, transaction_config: TransactionConfig, + evm_config: EvmConfig, ) -> Result { let config = cfg.value(); let metrics = registry.map(Metrics::new).transpose()?; @@ -464,6 +468,7 @@ impl Storage { metrics, chain_name_hash: ChainNameDigest::from_chain_name(network_name), transaction_config, + evm_config, state: ComponentState::Uninitialized, protocol_version, force_resync, @@ -624,6 +629,7 @@ impl Storage { } let utilization = Self::calculate_block_utilization( &self.transaction_config, + &self.evm_config, &block, &map, ); @@ -1028,6 +1034,7 @@ impl Storage { if let Some(block) = maybe_block { let utilization = Self::calculate_block_utilization( &self.transaction_config, + &self.evm_config, &block, &execution_results, ); @@ -1340,8 +1347,12 @@ impl Storage { let era_id = block.era_id(); let block_hash = txn.write_block(block)?; let _ = txn.write_approvals_hashes(approvals_hashes)?; - let utilization = - Self::calculate_block_utilization(&self.transaction_config, block, &execution_results); + let utilization = Self::calculate_block_utilization( + &self.transaction_config, + &self.evm_config, + block, + &execution_results, + ); let block_info = BlockHashHeightAndEra::new(block_hash, block.height(), block.era_id()); debug!("Utilization for block is {utilization}"); @@ -2307,12 +2318,14 @@ impl Storage { fn calculate_block_utilization( transaction_config_input: impl Borrow, + evm_config_input: impl Borrow, block: &Block, execution_results: &HashMap, ) -> u64 { let transaction_config = transaction_config_input.borrow(); - let block_utilization_score = block.block_utilization(transaction_config); - let has_hit_slot_limit = block.has_hit_slot_capacity(transaction_config); + let evm_config = evm_config_input.borrow(); + let block_utilization_score = block.block_utilization(transaction_config, evm_config); + let has_hit_slot_limit = block.has_hit_slot_capacity(transaction_config, evm_config); let utilization = if has_hit_slot_limit { debug!("Block is at slot capacity, using slot utilization score"); diff --git a/node/src/components/storage/tests.rs b/node/src/components/storage/tests.rs index 9e9dd1ff47..7267d1cda0 100644 --- a/node/src/components/storage/tests.rs +++ b/node/src/components/storage/tests.rs @@ -25,10 +25,10 @@ use casper_types::{ testing::TestRng, ApprovalsHash, AvailableBlockRange, Block, BlockHash, BlockHeader, BlockHeaderWithSignatures, BlockSignatures, BlockSignaturesV2, BlockV2, ChainNameDigest, Chainspec, ChainspecRawBytes, - Deploy, DeployHash, Digest, EraId, ExecutionInfo, FinalitySignature, FinalitySignatureV2, Gas, - InitiatorAddr, ProtocolVersion, PublicKey, SecretKey, TestBlockBuilder, TestBlockV1Builder, - TimeDiff, Timestamp, Transaction, TransactionConfig, TransactionHash, TransactionV1Hash, - Transfer, TransferV2, U512, + Deploy, DeployHash, Digest, EraId, EvmConfig, ExecutionInfo, FinalitySignature, + FinalitySignatureV2, Gas, InitiatorAddr, ProtocolVersion, PublicKey, SecretKey, + TestBlockBuilder, TestBlockV1Builder, TimeDiff, Timestamp, Transaction, TransactionConfig, + TransactionHash, TransactionV1Hash, Transfer, TransferV2, U512, }; use tempfile::tempdir; @@ -205,6 +205,7 @@ fn storage_fixture(harness: &ComponentHarness) -> Storage { None, false, TransactionConfig::default(), + EvmConfig::default(), ) .expect("could not create storage component fixture"); storage.initialize_for_test(); @@ -250,6 +251,7 @@ fn storage_fixture_from_parts( None, false, TransactionConfig::default(), + EvmConfig::default(), ) .expect("could not create storage component fixture from parts"); storage.initialize_for_test(); @@ -281,6 +283,7 @@ fn storage_fixture_with_force_resync(cfg: &WithDir) -> Storage { None, true, TransactionConfig::default(), + EvmConfig::default(), ) .expect("could not create storage component fixture"); storage.initialize_for_test(); @@ -1827,6 +1830,7 @@ fn should_create_subdir_named_after_network() { None, false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); diff --git a/node/src/components/transaction_acceptor.rs b/node/src/components/transaction_acceptor.rs index d8f2680ccd..04f7c61b8b 100644 --- a/node/src/components/transaction_acceptor.rs +++ b/node/src/components/transaction_acceptor.rs @@ -187,6 +187,7 @@ impl TransactionAcceptor { &input_transaction, self.chainspec.as_ref().core_config.pricing_handling, transaction_config, + &self.chainspec.as_ref().evm_config, ); let meta_transaction = match maybe_meta_transaction { Ok(transaction) => transaction, diff --git a/node/src/components/transaction_acceptor/tests.rs b/node/src/components/transaction_acceptor/tests.rs index e547248b22..9e099b3ddf 100644 --- a/node/src/components/transaction_acceptor/tests.rs +++ b/node/src/components/transaction_acceptor/tests.rs @@ -44,11 +44,11 @@ use casper_types::{ evm, global_state::TrieMerkleProof, testing::TestRng, - Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, EvmTransaction, - EvmTransactionError, Groups, HashAddr, InvalidDeploy, InvalidTransaction, InvalidTransactionV1, - Key, PackageAddr, PricingHandling, PricingMode, ProtocolVersion, PublicKey, SecretKey, - StoredValue, TestBlockBuilder, TimeDiff, Timestamp, Transaction, TransactionArgs, - TransactionConfig, TransactionRuntimeParams, TransactionV1, URef, + Block, BlockV2, CLValue, Chainspec, ChainspecRawBytes, Contract, Deploy, EraId, EvmConfig, + EvmTransaction, EvmTransactionError, Groups, HashAddr, InvalidDeploy, InvalidTransaction, + InvalidTransactionV1, Key, PackageAddr, PricingHandling, PricingMode, ProtocolVersion, + PublicKey, SecretKey, StoredValue, TestBlockBuilder, TimeDiff, Timestamp, Transaction, + TransactionArgs, TransactionConfig, TransactionRuntimeParams, TransactionV1, URef, DEFAULT_BASELINE_MOTES_AMOUNT, }; @@ -1443,6 +1443,7 @@ impl reactor::Reactor for Reactor { Some(registry), false, TransactionConfig::default(), + EvmConfig::default(), ) .unwrap(); storage.initialize_for_test(); @@ -1530,6 +1531,7 @@ fn inject_balance_check_for_peer( &txn, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, ) .unwrap(); |effect_builder: EffectBuilder| { diff --git a/node/src/components/transaction_buffer.rs b/node/src/components/transaction_buffer.rs index f1f066d000..6f965538c2 100644 --- a/node/src/components/transaction_buffer.rs +++ b/node/src/components/transaction_buffer.rs @@ -412,7 +412,7 @@ impl TransactionBuffer { let mut buckets: HashMap<_, Vec<_>> = HashMap::new(); for (transaction_hash, footprint) in proposable { buckets - .entry(&footprint.payload_hash) + .entry(footprint.payload_hash()) .and_modify(|vec| vec.push((*transaction_hash, footprint))) .or_insert(vec![(*transaction_hash, footprint)]); } @@ -431,6 +431,7 @@ impl TransactionBuffer { None => { return AppendableBlock::new( self.chainspec.transaction_config.clone(), + self.chainspec.evm_config.clone(), self.chainspec.vacancy_config.min_gas_price, timestamp, ); @@ -438,6 +439,7 @@ impl TransactionBuffer { }; let mut ret = AppendableBlock::new( self.chainspec.transaction_config.clone(), + self.chainspec.evm_config.clone(), current_era_gas_price, timestamp, ); @@ -498,7 +500,7 @@ impl TransactionBuffer { continue; } - let has_multiple_approvals = footprint.approvals.len() > 1; + let has_multiple_approvals = footprint.approvals_count() > 1; match ret.add_transaction(footprint) { Ok(_) => { debug!(%transaction_hash, "TransactionBuffer: proposing transaction"); @@ -576,7 +578,7 @@ impl TransactionBuffer { AddError::FutureDatedDeploy => { error!( ?transaction_hash, - %footprint.timestamp, + timestamp = %footprint.timestamp(), "TransactionBuffer: skipping transaction with future dated deploy" ); // keep iterating @@ -757,6 +759,7 @@ where None => responder .respond(AppendableBlock::new( self.chainspec.transaction_config.clone(), + self.chainspec.evm_config.clone(), self.chainspec.vacancy_config.min_gas_price, timestamp, )) diff --git a/node/src/reactor/main_reactor.rs b/node/src/reactor/main_reactor.rs index d5dacc2737..64e95fb1a9 100644 --- a/node/src/reactor/main_reactor.rs +++ b/node/src/reactor/main_reactor.rs @@ -1188,6 +1188,7 @@ impl reactor::Reactor for MainReactor { Some(registry), config.node.force_resync, chainspec.transaction_config.clone(), + chainspec.evm_config.clone(), )?; let allow_handshake = config.node.sync_handling != SyncHandling::Isolated; diff --git a/node/src/reactor/main_reactor/tests/transactions.rs b/node/src/reactor/main_reactor/tests/transactions.rs index ecf28a86cf..ab7ee9ce8f 100644 --- a/node/src/reactor/main_reactor/tests/transactions.rs +++ b/node/src/reactor/main_reactor/tests/transactions.rs @@ -37,7 +37,7 @@ use casper_types::{ bytesrepr::{Bytes, ToBytes}, evm, execution::ExecutionResultV1, - EvmAddr, EvmConfig, EvmSpec, EvmTransaction, DEFAULT_WEI_PER_MOTE, + EvmAddr, EvmConfig, EvmSpec, EvmTransaction, TransactionLaneDefinition, DEFAULT_WEI_PER_MOTE, }; pub(crate) static ALICE_SECRET_KEY: Lazy> = Lazy::new(|| { @@ -1235,9 +1235,16 @@ async fn should_execute_evm_transaction_and_store_receipt() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::Burn); let mut test = SingleTransactionTestCase::new( @@ -1282,10 +1289,10 @@ async fn should_execute_evm_transaction_and_store_receipt() { ); assert!(execution_result.receipt.gas_used > 0); let max_fee_amount = evm_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("max EVM fee should fit"); let consumed_fee_amount = evm_transaction - .fee_amount(execution_result.receipt.gas_used, &evm_config) + .fee_amount(execution_result.receipt.gas_used, &evm_config.fee_config()) .expect("consumed EVM fee should fit"); assert!(consumed_fee_amount < max_fee_amount); assert_eq!(execution_result.cost, max_fee_amount); @@ -1318,6 +1325,105 @@ async fn should_execute_evm_transaction_and_store_receipt() { ); } +fn signed_evm_value_transfer_transaction_with_nonce( + chain_id: u64, + nonce: u64, + recipient: evm::Address, + gas_limit: u64, + value: u64, +) -> EvmTransaction { + let transaction = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: EVM_TEST_GAS_PRICE, + gas_limit, + to: TxKind::Call(AlloyAddress::from(recipient.value())), + value: U256::from(value) * U256::from(DEFAULT_WEI_PER_MOTE), + input: AlloyBytes::new(), + }; + signed_evm_legacy_transaction(transaction) +} + +/// Proves that EVM transactions are assigned to the smallest `evm.transaction_lanes` lane +/// that can accommodate their gas limit, analogous to how wasm transactions are sized into +/// `transactions.v1.wasm_lanes`. +#[tokio::test] +async fn should_assign_evm_transactions_to_correctly_sized_lanes() { + const SMALL_LANE_ID: u8 = 100; + const LARGE_LANE_ID: u8 = 101; + const SMALL_LANE_GAS_LIMIT: u64 = 100_000; + + 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, + transaction_lanes: vec![ + TransactionLaneDefinition::new( + SMALL_LANE_ID, + u64::MAX, + u64::MAX, + SMALL_LANE_GAS_LIMIT, + 50, + ), + TransactionLaneDefinition::new(LARGE_LANE_ID, u64::MAX, u64::MAX, u64::MAX, 50), + ], + }; + let config = SingleTransactionTestCase::default_test_config() + .with_evm_config(evm_config.clone()) + .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 recipient = evm::Address::new([0x22; evm::ADDRESS_LENGTH]); + + // Sized to fit only the small lane's gas limit. + let small_transaction = signed_evm_value_transfer_transaction_with_nonce( + evm_config.chain_id, + 0, + recipient, + SMALL_LANE_GAS_LIMIT, + 0, + ); + let sender = small_transaction.from(); + seed_evm_account(&mut test.fixture, sender, U512::from(EVM_INITIAL_BALANCE)); + + let (small_txn_hash, _, small_result) = test + .send_transaction(Transaction::from(small_transaction)) + .await; + assert!(matches!(small_result, ExecutionResult::Evm(_))); + test.fixture + .assert_execution_in_lane(&small_txn_hash, SMALL_LANE_ID, Duration::from_secs(10)) + .await; + + // Exceeds the small lane's gas limit, so it must land in the large lane instead. + let large_transaction = signed_evm_value_transfer_transaction_with_nonce( + evm_config.chain_id, + 1, + recipient, + SMALL_LANE_GAS_LIMIT + 1, + 0, + ); + let (large_txn_hash, _, large_result) = test + .send_transaction(Transaction::from(large_transaction)) + .await; + assert!(matches!(large_result, ExecutionResult::Evm(_))); + test.fixture + .assert_execution_in_lane(&large_txn_hash, LARGE_LANE_ID, Duration::from_secs(10)) + .await; +} + #[tokio::test] async fn should_prelink_ed25519_proposer_coinbase_for_evm_execution() { let evm_config = EvmConfig { @@ -1327,9 +1433,16 @@ async fn should_prelink_ed25519_proposer_coinbase_for_evm_execution() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::Burn); let mut test = SingleTransactionTestCase::new( @@ -1418,9 +1531,16 @@ async fn should_apply_casper_fee_and_refund_handling_to_evm_transaction() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::Refund { refund_ratio: Ratio::new(1, 4), }) @@ -1450,10 +1570,10 @@ async fn should_apply_casper_fee_and_refund_handling_to_evm_transaction() { }; let max_fee_amount = evm_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("max EVM fee should fit"); let consumed_fee_amount = evm_transaction - .fee_amount(execution_result.receipt.gas_used, &evm_config) + .fee_amount(execution_result.receipt.gas_used, &evm_config.fee_config()) .expect("consumed EVM fee should fit"); let expected_refund = (max_fee_amount - consumed_fee_amount) / U512::from(4); let expected_fee = max_fee_amount - expected_refund; @@ -1480,9 +1600,16 @@ async fn should_apply_no_refund_to_eip1559_max_fee_headroom() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::PayToProposer); let mut test = SingleTransactionTestCase::new( @@ -1517,7 +1644,7 @@ async fn should_apply_no_refund_to_eip1559_max_fee_headroom() { }; let maximum_fee = evm_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("maximum EVM fee should fit"); assert_eq!(execution_result.receipt.status, evm::ReceiptStatus::Success); assert_eq!(execution_result.receipt.gas_used, gas_limit); @@ -1538,9 +1665,16 @@ async fn should_require_balance_for_eip1559_signed_maximum() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::Refund { refund_ratio: Ratio::new(1, 1), }) @@ -1568,10 +1702,10 @@ async fn should_require_balance_for_eip1559_signed_maximum() { ); let sender = evm_transaction.from(); let base_fee_amount = evm_transaction - .fee_amount(gas_limit, &evm_config) + .fee_amount(gas_limit, &evm_config.fee_config()) .expect("base fee should fit"); let maximum_fee_amount = evm_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("maximum fee should fit"); assert!(base_fee_amount < maximum_fee_amount); seed_evm_account(&mut test.fixture, sender, base_fee_amount); @@ -1606,9 +1740,16 @@ async fn should_apply_refund_policy_to_evm_revert_and_halt() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::Refund { refund_ratio: Ratio::new(1, 4), }) @@ -1662,7 +1803,7 @@ async fn should_apply_refund_policy_to_evm_revert_and_halt() { let revert_transaction = signed_evm_call_transaction(evm_config.chain_id, 2, revert_contract, 0, Vec::new()); let revert_maximum_fee = revert_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("maximum EVM fee should fit"); let (_hash, revert_height, revert_result) = test .send_transaction(Transaction::from(revert_transaction.clone())) @@ -1671,7 +1812,7 @@ async fn should_apply_refund_policy_to_evm_revert_and_halt() { panic!("expected EVM execution result"); }; let revert_consumed_fee = revert_transaction - .fee_amount(revert_result.receipt.gas_used, &evm_config) + .fee_amount(revert_result.receipt.gas_used, &evm_config.fee_config()) .expect("consumed EVM fee should fit"); let expected_revert_refund = (revert_maximum_fee - revert_consumed_fee) / U512::from(4); assert_eq!(revert_result.receipt.status, evm::ReceiptStatus::Revert); @@ -1686,7 +1827,7 @@ async fn should_apply_refund_policy_to_evm_revert_and_halt() { let halt_transaction = signed_evm_call_transaction(evm_config.chain_id, 3, halt_contract, 0, Vec::new()); let halt_maximum_fee = halt_transaction - .max_fee_amount(&evm_config) + .max_fee_amount(&evm_config.fee_config()) .expect("maximum EVM fee should fit"); let (_hash, halt_height, halt_result) = test .send_transaction(Transaction::from(halt_transaction.clone())) @@ -1715,9 +1856,16 @@ async fn should_reject_evm_transaction_when_value_and_fee_exceed_balance() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::Burn); let mut test = SingleTransactionTestCase::new( @@ -1782,8 +1930,16 @@ async fn should_not_seed_evm_accounts_at_genesis() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; - let config = SingleTransactionTestCase::default_test_config().with_evm_config(evm_config); + let config = + SingleTransactionTestCase::default_test_config().with_evm_config(evm_config.clone()); let alice_secret_key = Arc::new( SecretKey::secp256k1_from_bytes([0x11; SecretKey::SECP256K1_LENGTH]) .expect("secp256k1 key should be valid"), @@ -1833,9 +1989,16 @@ async fn should_transfer_to_evm_address_with_native_transfer() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_pricing_handling(PricingHandling::Fixed) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::NoFee); @@ -1906,9 +2069,16 @@ async fn should_reject_native_transfer_to_evm_contract_address() { block_gas_limit: 30_000_000, base_fee: 1, wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: vec![TransactionLaneDefinition::new( + 100, + u64::MAX, + u64::MAX, + u64::MAX, + 100, + )], }; let config = SingleTransactionTestCase::default_test_config() - .with_evm_config(evm_config) + .with_evm_config(evm_config.clone()) .with_pricing_handling(PricingHandling::Fixed) .with_refund_handling(RefundHandling::NoRefund) .with_fee_handling(FeeHandling::Burn); @@ -3369,6 +3539,7 @@ async fn should_gas_hold_fee_erroneous_wasm(txn_pricing_mode: PricingMode) { &txn, test.chainspec().core_config.pricing_handling, &test.chainspec().transaction_config, + &test.chainspec().evm_config, ) .unwrap(); // Fixed transaction pricing. diff --git a/node/src/testing/fake_transaction_acceptor.rs b/node/src/testing/fake_transaction_acceptor.rs index a6fa6a86a4..a0e0467f9a 100644 --- a/node/src/testing/fake_transaction_acceptor.rs +++ b/node/src/testing/fake_transaction_acceptor.rs @@ -66,6 +66,7 @@ impl FakeTransactionAcceptor { &transaction, self.chainspec.core_config.pricing_handling, &self.chainspec.transaction_config, + &self.chainspec.evm_config, ) .unwrap(); let event_metadata = Box::new(EventMetadata::new( diff --git a/node/src/types/appendable_block.rs b/node/src/types/appendable_block.rs index d328d6f2e3..6e75fe2b57 100644 --- a/node/src/types/appendable_block.rs +++ b/node/src/types/appendable_block.rs @@ -8,8 +8,8 @@ use itertools::Itertools; use thiserror::Error; use casper_types::{ - Approval, Gas, PublicKey, RewardedSignatures, Timestamp, TransactionConfig, TransactionHash, - AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, U512, + Approval, EvmConfig, Gas, PublicKey, RewardedSignatures, Timestamp, TransactionConfig, + TransactionHash, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, MINT_LANE_ID, U512, }; use super::{BlockPayload, TransactionFootprint, VariantMismatch}; @@ -40,6 +40,7 @@ pub(crate) enum AddError { #[derive(Clone, Eq, PartialEq, DataSize, Debug)] pub(crate) struct AppendableBlock { transaction_config: TransactionConfig, + evm_config: EvmConfig, current_gas_price: u8, transactions: BTreeMap, timestamp: Timestamp, @@ -49,11 +50,13 @@ impl AppendableBlock { /// Creates an empty `AppendableBlock`. pub(crate) fn new( transaction_config: TransactionConfig, + evm_config: EvmConfig, current_gas_price: u8, timestamp: Timestamp, ) -> Self { AppendableBlock { transaction_config, + evm_config, current_gas_price, transactions: BTreeMap::new(), timestamp, @@ -68,30 +71,33 @@ impl AppendableBlock { if self .transactions .keys() - .contains(&footprint.transaction_hash) + .contains(&footprint.transaction_hash()) { return Err(AddError::Duplicate); } - if footprint.ttl > self.transaction_config.max_ttl { + if footprint.ttl() > self.transaction_config.max_ttl { return Err(AddError::ExcessiveTtl); } - if footprint.timestamp > self.timestamp { + if footprint.timestamp() > self.timestamp { return Err(AddError::FutureDatedDeploy); } - let expires = footprint.timestamp.saturating_add(footprint.ttl); + let expires = footprint.timestamp().saturating_add(footprint.ttl()); if expires < self.timestamp { return Err(AddError::Expired); } - let lane_id = footprint.lane_id; - let limit = self - .transaction_config - .transaction_v1_config - .get_max_transaction_count(lane_id); + let lane_id = footprint.lane_id(); + let limit = if self.evm_config.is_supported(lane_id) { + self.evm_config.get_max_transaction_count(lane_id) + } else { + self.transaction_config + .transaction_v1_config + .get_max_transaction_count(lane_id) + }; // check total count by category let count = self .transactions .iter() - .filter(|(_, item)| item.lane_id == lane_id) + .filter(|(_, item)| item.lane_id() == lane_id) .count(); if count.checked_add(1).ok_or(AddError::Count(lane_id))? > limit as usize { return Err(AddError::Count(lane_id)); @@ -100,10 +106,10 @@ impl AppendableBlock { let gas_limit: U512 = self .transactions .values() - .map(|item| item.gas_limit.value()) + .map(|item| item.gas_limit().value()) .sum(); if gas_limit - .checked_add(footprint.gas_limit.value()) + .checked_add(footprint.gas_limit().value()) .ok_or(AddError::GasLimit)? > U512::from(self.transaction_config.block_gas_limit) { @@ -113,10 +119,10 @@ impl AppendableBlock { let size: usize = self .transactions .values() - .map(|item| item.size_estimate) + .map(|item| item.size_estimate()) .sum(); if size - .checked_add(footprint.size_estimate) + .checked_add(footprint.size_estimate()) .ok_or(AddError::BlockSize)? > self.transaction_config.max_block_size as usize { @@ -136,7 +142,7 @@ impl AppendableBlock { return Err(AddError::ApprovalCount); } self.transactions - .insert(footprint.transaction_hash, footprint.clone()); + .insert(footprint.transaction_hash(), footprint.clone()); Ok(()) } @@ -160,8 +166,8 @@ impl AppendableBlock { items: &BTreeMap, ) { let mut ret = vec![]; - for (x, y) in items.iter().filter(|(_, y)| y.lane_id == lane) { - ret.push((*x, y.approvals.clone())); + for (x, y) in items.iter().filter(|(_, y)| y.lane_id() == lane) { + ret.push((*x, y.approvals().clone())); } if !ret.is_empty() { collater.insert(lane, ret); @@ -181,6 +187,14 @@ impl AppendableBlock { { collate(lane_id, &mut transactions, &footprints); } + for lane_id in self + .evm_config + .transaction_lanes() + .iter() + .map(|lane| lane.id()) + { + collate(lane_id, &mut transactions, &footprints); + } BlockPayload::new( transactions, @@ -198,7 +212,7 @@ impl AppendableBlock { fn category_lane(&self, lane: u8) -> usize { self.transactions .iter() - .filter(|(_, f)| f.lane_id == lane) + .filter(|(_, f)| f.lane_id() == lane) .count() } @@ -218,7 +232,7 @@ impl Display for AppendableBlock { let total_gas_limit: Gas = self .transactions .values() - .map(|f| f.gas_limit) + .map(|f| f.gas_limit()) .try_fold(Gas::new(0), |acc, gas| acc.checked_add(gas)) .unwrap_or(Gas::MAX); let total_approvals_count: usize = self @@ -226,7 +240,8 @@ impl Display for AppendableBlock { .values() .map(|f| f.approvals_count()) .sum(); - let total_size_estimate: usize = self.transactions.values().map(|f| f.size_estimate).sum(); + let total_size_estimate: usize = + self.transactions.values().map(|f| f.size_estimate()).sum(); write!( formatter, @@ -246,13 +261,20 @@ impl Display for AppendableBlock { #[cfg(test)] mod tests { - use casper_types::{testing::TestRng, SingleBlockRewardedSignatures, TimeDiff}; + use casper_types::{ + testing::TestRng, SingleBlockRewardedSignatures, TimeDiff, TransactionLaneDefinition, + }; use crate::testing::LARGE_WASM_LANE_ID; use super::*; use std::collections::HashSet; + // An arbitrary id for a test EVM lane; the actual numeric value carries no meaning to the + // code, which decides "is this lane an EVM lane" solely by membership in + // `evm_config.transaction_lanes()`. + const TEST_EVM_LANE_ID: u8 = 100; + impl AppendableBlock { pub(crate) fn transaction_hashes(&self) -> HashSet { self.transactions.keys().copied().collect() @@ -262,8 +284,17 @@ mod tests { #[test] pub fn should_build_block_payload_from_all_transactions() { let mut test_rng = TestRng::new(); + let mut evm_config = EvmConfig::default(); + evm_config.set_transaction_lanes(vec![TransactionLaneDefinition::new( + TEST_EVM_LANE_ID, + u64::MAX, + u64::MAX, + u64::MAX, + 10, + )]); let mut appendable_block = AppendableBlock::new( TransactionConfig::default(), + evm_config, 0, Timestamp::now() + TimeDiff::from_millis(15000), ); @@ -274,6 +305,7 @@ mod tests { TransactionFootprint::random_of_lane(INSTALL_UPGRADE_LANE_ID, &mut test_rng); let large_wasm_footprint = TransactionFootprint::random_of_lane(LARGE_WASM_LANE_ID, &mut test_rng); + let evm_footprint = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); let signatures = RewardedSignatures::new(vec![SingleBlockRewardedSignatures::random( &mut test_rng, 2, @@ -290,14 +322,157 @@ mod tests { appendable_block .add_transaction(&large_wasm_footprint) .unwrap(); + appendable_block.add_transaction(&evm_footprint).unwrap(); let block_payload = appendable_block.into_block_payload(vec![], signatures.clone(), false); let transaction_hashes: BTreeSet = block_payload.all_transaction_hashes().collect(); - assert!(transaction_hashes.contains(&transfer_footprint.transaction_hash)); - assert!(transaction_hashes.contains(&auction_footprint.transaction_hash)); - assert!(transaction_hashes.contains(&install_upgrade_footprint.transaction_hash)); - assert!(transaction_hashes.contains(&large_wasm_footprint.transaction_hash)); - assert_eq!(transaction_hashes.len(), 4); + assert!(transaction_hashes.contains(&transfer_footprint.transaction_hash())); + assert!(transaction_hashes.contains(&auction_footprint.transaction_hash())); + assert!(transaction_hashes.contains(&install_upgrade_footprint.transaction_hash())); + assert!(transaction_hashes.contains(&large_wasm_footprint.transaction_hash())); + assert!(transaction_hashes.contains(&evm_footprint.transaction_hash())); + assert_eq!(transaction_hashes.len(), 5); assert_eq!(*block_payload.rewarded_signatures(), signatures); } + + #[test] + fn should_reject_evm_transaction_exceeding_lane_count_limit() { + let mut test_rng = TestRng::new(); + let mut appendable_block = evm_appendable_block(2, TransactionConfig::default()); + + let first = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + let second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + let third = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + + appendable_block.add_transaction(&first).unwrap(); + appendable_block.add_transaction(&second).unwrap(); + assert!(matches!( + appendable_block.add_transaction(&third), + Err(AddError::Count(lane_id)) if lane_id == TEST_EVM_LANE_ID + )); + } + + #[test] + fn should_reject_evm_transaction_exceeding_block_gas_limit_even_when_lane_allows_more() { + let mut test_rng = TestRng::new(); + // The EVM lane's own count limit is generous; the block-wide gas budget is what + // should actually bind here. + let transaction_config = TransactionConfig { + block_gas_limit: 150, + ..Default::default() + }; + let mut appendable_block = evm_appendable_block(1000, transaction_config); + + let mut first = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + first.set_gas_limit(Gas::new(100)); + let mut second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + second.set_gas_limit(Gas::new(100)); + + appendable_block.add_transaction(&first).unwrap(); + // Total gas would be 200 > 150, even though the lane count limit (1000) is nowhere + // near being hit. + assert!(matches!( + appendable_block.add_transaction(&second), + Err(AddError::GasLimit) + )); + assert_eq!(appendable_block.transaction_count(), 1); + } + + #[test] + fn should_reject_evm_transaction_exceeding_block_size_limit_even_when_lane_allows_more() { + let mut test_rng = TestRng::new(); + // The EVM lane's own count limit is generous; the block-wide size budget is what + // should actually bind here. + let transaction_config = TransactionConfig { + max_block_size: 1500, + ..Default::default() + }; + let mut appendable_block = evm_appendable_block(1000, transaction_config); + + let mut first = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + first.set_size_estimate(1000); + let mut second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + second.set_size_estimate(1000); + + appendable_block.add_transaction(&first).unwrap(); + // Total size would be 2000 > 1500, even though the lane count limit (1000) is + // nowhere near being hit. + assert!(matches!( + appendable_block.add_transaction(&second), + Err(AddError::BlockSize) + )); + assert_eq!(appendable_block.transaction_count(), 1); + } + + #[test] + fn should_reject_evm_transaction_exceeding_block_approval_limit_even_when_lane_allows_more() { + let mut test_rng = TestRng::new(); + let transaction_config = TransactionConfig { + block_max_approval_count: 1, + ..Default::default() + }; + let mut appendable_block = evm_appendable_block(1000, transaction_config); + + let first = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + let second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + + appendable_block.add_transaction(&first).unwrap(); + assert!(matches!( + appendable_block.add_transaction(&second), + Err(AddError::ApprovalCount) + )); + assert_eq!(appendable_block.transaction_count(), 1); + } + + #[test] + fn should_cap_mixed_lane_transactions_at_shared_block_gas_quota() { + let mut test_rng = TestRng::new(); + // Every lane's own count limit is generous; only the shared block gas budget + // should actually bind here, regardless of which lanes the transactions come from. + let transaction_config = TransactionConfig { + block_gas_limit: 250, + ..Default::default() + }; + let mut appendable_block = evm_appendable_block(1000, transaction_config); + + let mut mint_footprint = TransactionFootprint::random_of_lane(MINT_LANE_ID, &mut test_rng); + mint_footprint.set_gas_limit(Gas::new(100)); + let mut wasm_footprint = + TransactionFootprint::random_of_lane(LARGE_WASM_LANE_ID, &mut test_rng); + wasm_footprint.set_gas_limit(Gas::new(100)); + let mut evm_footprint = + TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + evm_footprint.set_gas_limit(Gas::new(100)); + + appendable_block.add_transaction(&mint_footprint).unwrap(); + appendable_block.add_transaction(&wasm_footprint).unwrap(); + // Adding the EVM transaction would bring total gas to 300 > 250. None of the + // individual lane count limits are anywhere close to being hit; only the shared + // block-wide quota, accumulated across mixed lanes, should reject it. + assert!(matches!( + appendable_block.add_transaction(&evm_footprint), + Err(AddError::GasLimit) + )); + assert_eq!(appendable_block.transaction_count(), 2); + } + + fn evm_appendable_block( + lane_max_transaction_count: u64, + transaction_config: TransactionConfig, + ) -> AppendableBlock { + let mut evm_config = EvmConfig::default(); + evm_config.set_transaction_lanes(vec![TransactionLaneDefinition::new( + TEST_EVM_LANE_ID, + u64::MAX, + u64::MAX, + u64::MAX, + lane_max_transaction_count, + )]); + AppendableBlock::new( + transaction_config, + evm_config, + 0, + Timestamp::now() + TimeDiff::from_millis(15000), + ) + } } diff --git a/node/src/types/block/executable_block.rs b/node/src/types/block/executable_block.rs index 502a637108..8e78cc96b1 100644 --- a/node/src/types/block/executable_block.rs +++ b/node/src/types/block/executable_block.rs @@ -103,7 +103,11 @@ impl ExecutableBlock { pub(crate) fn calc_utilization_score(&self, chainspec: &Chainspec) -> Option { let cfg = &chainspec.transaction_config.transaction_v1_config; - let per_block_capacity = cfg.get_max_block_count(); + let per_block_capacity = cfg.get_max_block_count() + + chainspec + .evm_config + .get_max_evm_transaction_count() + .unwrap_or(0); let max_block_size = chainspec.transaction_config.max_block_size as u64; let block_gas_limit = chainspec.transaction_config.block_gas_limit; @@ -116,7 +120,11 @@ impl ExecutableBlock { .iter() .map(|transaction| (transaction, *lane_id)), ); - let max_count = cfg.get_max_transaction_count(*lane_id); + let max_count = if chainspec.evm_config.is_supported(*lane_id) { + chainspec.evm_config.get_max_transaction_count(*lane_id) + } else { + cfg.get_max_transaction_count(*lane_id) + }; if max_count == transactions.len() as u64 { has_hit_slot_limit = true; } diff --git a/node/src/types/transaction/meta_transaction.rs b/node/src/types/transaction/meta_transaction.rs index bb81143a81..f7674fe6b9 100644 --- a/node/src/types/transaction/meta_transaction.rs +++ b/node/src/types/transaction/meta_transaction.rs @@ -6,10 +6,10 @@ use casper_execution_engine::engine_state::{SessionDataDeploy, SessionDataV1, Se #[cfg(test)] use casper_types::InvalidTransactionV1; use casper_types::{ - account::AccountHash, bytesrepr::ToBytes, Approval, Chainspec, Digest, EvmTransaction, - ExecutableDeployItem, Gas, GasLimited, HashAddr, InitiatorAddr, InvalidTransaction, Phase, - PricingHandling, PricingMode, TimeDiff, Timestamp, Transaction, TransactionArgs, - TransactionConfig, TransactionEntryPoint, TransactionHash, TransactionTarget, + account::AccountHash, bytesrepr::ToBytes, Approval, Chainspec, Digest, EvmConfig, + EvmTransaction, ExecutableDeployItem, Gas, GasLimited, HashAddr, InitiatorAddr, + InvalidTransaction, Phase, PricingHandling, PricingMode, TimeDiff, Timestamp, Transaction, + TransactionArgs, TransactionConfig, TransactionEntryPoint, TransactionHash, TransactionTarget, INSTALL_UPGRADE_LANE_ID, }; use core::fmt::{self, Debug, Display, Formatter}; @@ -277,6 +277,7 @@ impl MetaTransaction { transaction: &Transaction, pricing_handling: PricingHandling, transaction_config: &TransactionConfig, + evm_config: &EvmConfig, ) -> Result { match transaction { Transaction::Deploy(deploy) => MetaDeploy::from_deploy( @@ -291,8 +292,7 @@ impl MetaTransaction { ) .map(MetaTransaction::V1), Transaction::Evm(evm) => { - MetaEvmTransaction::from_evm_transaction(evm, transaction_config) - .map(MetaTransaction::Evm) + MetaEvmTransaction::from_evm_transaction(evm, evm_config).map(MetaTransaction::Evm) } } } @@ -500,6 +500,7 @@ pub(crate) fn calculate_transaction_lane_for_transaction( transaction, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, )?; Ok(meta.transaction_lane()) } @@ -508,6 +509,7 @@ pub(crate) fn calculate_transaction_lane_for_transaction( transaction, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, )?; Ok(meta.transaction_lane()) } @@ -556,7 +558,8 @@ mod tests { const CHAIN_ID: u64 = 7; const BASE_FEE: u64 = 1_000_000; const BASE_FEE_WEI: u128 = BASE_FEE as u128 * DEFAULT_WEI_PER_MOTE as u128; - const EVM_LANE: u8 = 4; + // An arbitrary id for a test EVM lane; the numeric value carries no special meaning. + const EVM_LANE: u8 = 100; #[test] fn evm_from_transaction_exposes_metadata() { @@ -567,6 +570,7 @@ mod tests { &transaction, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, ) .expect("EVM transaction metadata should be created"); @@ -611,16 +615,14 @@ mod tests { #[test] fn evm_from_transaction_requires_lane() { let mut chainspec = chainspec(); - chainspec - .transaction_config - .transaction_v1_config - .set_wasm_lanes(vec![]); + chainspec.evm_config.set_transaction_lanes(vec![]); let transaction = Transaction::from_evm(legacy_transaction(Some(CHAIN_ID), BASE_FEE_WEI, 21_000)); let error = MetaTransaction::from_transaction( &transaction, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, ) .expect_err("EVM transaction should need a lane"); assert!(matches!( @@ -1023,9 +1025,8 @@ mod tests { chainspec.evm_config.base_fee = BASE_FEE; chainspec.evm_config.block_gas_limit = 30_000_000; chainspec - .transaction_config - .transaction_v1_config - .set_wasm_lanes(vec![TransactionLaneDefinition::new( + .evm_config + .set_transaction_lanes(vec![TransactionLaneDefinition::new( EVM_LANE, u64::MAX, 10_000, @@ -1040,6 +1041,7 @@ mod tests { &Transaction::from_evm(evm_transaction), chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, ) .expect("EVM transaction metadata should be created") } @@ -1186,7 +1188,7 @@ mod proptests { TransactionLaneDefinition::new(3, u64::MAX / 2, 10000, u64::MAX / 2, 10), TransactionLaneDefinition::new(4, u64::MAX, 10000, u64::MAX, 10), ]); - let maybe_transaction = MetaTransaction::from_transaction(&transaction, PricingHandling::PaymentLimited, &transaction_config); + let maybe_transaction = MetaTransaction::from_transaction(&transaction, PricingHandling::PaymentLimited, &transaction_config, &EvmConfig::default()); prop_assert!(maybe_transaction.is_ok(), "{:?}", maybe_transaction); } } diff --git a/node/src/types/transaction/meta_transaction/meta_evm.rs b/node/src/types/transaction/meta_transaction/meta_evm.rs index db175c26f4..38999b63e4 100644 --- a/node/src/types/transaction/meta_transaction/meta_evm.rs +++ b/node/src/types/transaction/meta_transaction/meta_evm.rs @@ -1,9 +1,9 @@ use std::fmt::{self, Display, Formatter}; use casper_types::{ - bytesrepr::ToBytes, Approval, Chainspec, Digest, EvmTransaction, EvmTransactionError, - EvmTransactionKind, Gas, InitiatorAddr, InvalidTransaction, TimeDiff, Timestamp, - TransactionConfig, TransactionHash, + bytesrepr::ToBytes, Approval, Chainspec, Digest, EvmConfig, EvmTransaction, + EvmTransactionError, EvmTransactionKind, Gas, InitiatorAddr, InvalidTransaction, TimeDiff, + Timestamp, TransactionHash, }; use serde::Serialize; @@ -18,14 +18,14 @@ pub(crate) struct MetaEvmTransaction { impl MetaEvmTransaction { pub(crate) fn from_evm_transaction( transaction: &EvmTransaction, - transaction_config: &TransactionConfig, + evm_config: &EvmConfig, ) -> Result { - let lane_id = transaction_config - .transaction_v1_config - .wasm_lanes() - .iter() - .last() - .map(|lane| lane.id()) + let lane_id = evm_config + .get_evm_lane_id( + transaction.gas_limit(), + transaction.serialized_length() as u64, + transaction.input().len() as u64, + ) .ok_or(EvmTransactionError::MissingTransactionLane)?; let payload_hash = Digest::hash(transaction.signing_payload()?); Ok(MetaEvmTransaction { diff --git a/node/src/types/transaction/transaction_footprint.rs b/node/src/types/transaction/transaction_footprint.rs index 9b28ea04c1..53fe4a78b8 100644 --- a/node/src/types/transaction/transaction_footprint.rs +++ b/node/src/types/transaction/transaction_footprint.rs @@ -17,23 +17,23 @@ use std::collections::BTreeSet; /// The block footprint of a transaction. pub(crate) struct TransactionFootprint { /// The identifying hash. - pub(crate) transaction_hash: TransactionHash, + transaction_hash: TransactionHash, /// Transaction payload hash. - pub(crate) payload_hash: Digest, + payload_hash: Digest, /// The estimated gas consumption. - pub(crate) gas_limit: Gas, + gas_limit: Gas, /// The gas tolerance. - pub(crate) gas_price_tolerance: u8, + gas_price_tolerance: u8, /// The bytesrepr serialized length. - pub(crate) size_estimate: usize, + size_estimate: usize, /// The transaction lane_id. - pub(crate) lane_id: u8, + lane_id: u8, /// Timestamp of the transaction. - pub(crate) timestamp: Timestamp, + timestamp: Timestamp, /// Time to live for the transaction. - pub(crate) ttl: TimeDiff, + ttl: TimeDiff, /// The approvals. - pub(crate) approvals: BTreeSet, + approvals: BTreeSet, } impl TransactionFootprint { @@ -45,6 +45,7 @@ impl TransactionFootprint { transaction, chainspec.core_config.pricing_handling, &chainspec.transaction_config, + &chainspec.evm_config, )?; Self::new_from_meta_transaction(chainspec, &transaction) } @@ -56,11 +57,7 @@ impl TransactionFootprint { let gas_price_tolerance = transaction.gas_price_tolerance()?; let gas_limit = transaction.gas_limit(chainspec)?; let lane_id = transaction.transaction_lane(); - if !chainspec - .transaction_config - .transaction_v1_config - .is_supported(lane_id) - { + if !chainspec.is_supported(lane_id) { return Err(InvalidTransaction::V1( InvalidTransactionV1::InvalidTransactionLane(lane_id), )); @@ -90,6 +87,46 @@ impl TransactionFootprint { self } + /// The identifying hash. + pub(crate) fn transaction_hash(&self) -> TransactionHash { + self.transaction_hash + } + + /// Transaction payload hash. + pub(crate) fn payload_hash(&self) -> &Digest { + &self.payload_hash + } + + /// The estimated gas consumption. + pub(crate) fn gas_limit(&self) -> Gas { + self.gas_limit + } + + /// The bytesrepr serialized length. + pub(crate) fn size_estimate(&self) -> usize { + self.size_estimate + } + + /// The transaction lane_id. + pub(crate) fn lane_id(&self) -> u8 { + self.lane_id + } + + /// Timestamp of the transaction. + pub(crate) fn timestamp(&self) -> Timestamp { + self.timestamp + } + + /// Time to live for the transaction. + pub(crate) fn ttl(&self) -> TimeDiff { + self.ttl + } + + /// The approvals. + pub(crate) fn approvals(&self) -> &BTreeSet { + &self.approvals + } + /// The approval count, if known. pub(crate) fn approvals_count(&self) -> usize { self.approvals.len() @@ -133,6 +170,16 @@ impl TransactionFootprint { self.gas_price_tolerance } + #[cfg(test)] + pub(crate) fn set_gas_limit(&mut self, gas_limit: Gas) { + self.gas_limit = gas_limit; + } + + #[cfg(test)] + pub(crate) fn set_size_estimate(&mut self, size_estimate: usize) { + self.size_estimate = size_estimate; + } + #[cfg(test)] pub fn random_of_lane(lane_id: u8, rng: &mut TestRng) -> Self { let transaction_hash = TransactionHash::random(rng); diff --git a/node/src/utils/chain_specification.rs b/node/src/utils/chain_specification.rs index fa5e43e383..db791e772b 100644 --- a/node/src/utils/chain_specification.rs +++ b/node/src/utils/chain_specification.rs @@ -9,8 +9,8 @@ use tracing::{error, info, warn}; use casper_types::{ system::auction::VESTING_SCHEDULE_LENGTH_MILLIS, Chainspec, ConsensusProtocolName, CoreConfig, - ProtocolConfig, TimeDiff, TransactionConfig, AUCTION_LANE_ID, INSTALL_UPGRADE_LANE_ID, - MINIMUM_WEI_PER_MOTE, MINT_LANE_ID, + EvmConfig, ProtocolConfig, TimeDiff, TransactionConfig, TransactionV1Config, AUCTION_LANE_ID, + INSTALL_UPGRADE_LANE_ID, MINIMUM_WEI_PER_MOTE, MINT_LANE_ID, }; use crate::components::network; @@ -92,7 +92,11 @@ pub fn validate_chainspec(chainspec: &Chainspec) -> bool { network::within_message_size_limit_tolerance(chainspec) && validate_protocol_config(&chainspec.protocol_config) && validate_core_config(&chainspec.core_config) - && validate_transaction_config(&chainspec.transaction_config) + && validate_transaction_config(&chainspec.transaction_config, &chainspec.evm_config) + && validate_evm_transaction_lanes( + &chainspec.evm_config, + &chainspec.transaction_config.transaction_v1_config, + ) } /// Checks whether the values set in the config make sense and returns `false` if they don't. @@ -161,12 +165,16 @@ pub(crate) fn validate_core_config(core_config: &CoreConfig) -> bool { } /// Validates `TransactionConfig` parameters -pub(crate) fn validate_transaction_config(transaction_config: &TransactionConfig) -> bool { +pub(crate) fn validate_transaction_config( + transaction_config: &TransactionConfig, + evm_config: &EvmConfig, +) -> bool { // The total number of transactions should not exceed the number of approvals because each // transaction needs at least one approval to be valid. let total_txn_slots = transaction_config .transaction_v1_config - .get_max_block_count(); + .get_max_block_count() + + evm_config.get_max_evm_transaction_count().unwrap_or(0); if transaction_config.block_max_approval_count < total_txn_slots as u32 { return false; } @@ -205,6 +213,52 @@ pub(crate) fn validate_transaction_config(transaction_config: &TransactionConfig true } +/// Validates `evm.transaction_lanes` parameters. +/// +/// EVM lane ids must be unique among themselves, and must not collide with any of the +/// reserved native lane ids or any configured wasm lane id. There is no numeric convention +/// enforced on EVM lane ids beyond that; chainspec authors are free to pick any ids that +/// don't collide with the native/wasm lanes. +pub(crate) fn validate_evm_transaction_lanes( + evm_config: &EvmConfig, + transaction_v1_config: &TransactionV1Config, +) -> bool { + if evm_config.enabled && evm_config.transaction_lanes().is_empty() { + error!("EVM is enabled but evm.transaction_lanes chainspec config is empty."); + return false; + } + + let mut other_lane_ids: HashSet = RESERVED_LANE_IDS.iter().copied().collect(); + other_lane_ids.extend( + transaction_v1_config + .wasm_lanes() + .iter() + .map(|lane| lane.id()), + ); + + let mut seen_evm_lane_ids = HashSet::new(); + for evm_lane_config in evm_config.transaction_lanes().iter() { + let lane_id = evm_lane_config.id(); + if seen_evm_lane_ids.contains(&lane_id) { + error!( + "Found evm transaction lane configuration that has non-unique id. Duplicate value: {}", + lane_id + ); + return false; + } + seen_evm_lane_ids.insert(lane_id); + if other_lane_ids.contains(&lane_id) { + error!( + "One of the defined evm transaction lanes has declared an id that collides with \ + a native or wasm lane id. Offending lane id: {}", + lane_id + ); + return false; + } + } + true +} + #[cfg(test)] mod tests { use std::fs; @@ -491,7 +545,7 @@ mod tests { ..Default::default() }; assert!( - !validate_transaction_config(&transaction_config), + !validate_transaction_config(&transaction_config, &EvmConfig::default()), "max approval count that is not at least equal to sum of `block_max_[txn type]_count`s \ should be invalid" ); @@ -507,7 +561,7 @@ mod tests { ..Default::default() }; assert!( - validate_transaction_config(&transaction_config), + validate_transaction_config(&transaction_config, &EvmConfig::default()), "max approval count equal to sum of `block_max_[txn type]_count`s should be valid" ); @@ -521,7 +575,7 @@ mod tests { ..Default::default() }; assert!( - validate_transaction_config(&transaction_config), + validate_transaction_config(&transaction_config, &EvmConfig::default()), "max approval count greater than sum of `block_max_[txn type]_count`s should be valid" ); } @@ -758,7 +812,10 @@ mod tests { transaction_v1_config: v1_config.clone(), ..Default::default() }; - assert!(validate_transaction_config(&transaction_config)); + assert!(validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); let mut definition_2 = definition_2.clone(); definition_2.set_max_transaction_length(definition_1.max_transaction_length()); v1_config.set_wasm_lanes(vec![ @@ -770,7 +827,10 @@ mod tests { transaction_v1_config: v1_config, ..Default::default() }; - assert!(!validate_transaction_config(&transaction_config)); + assert!(!validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); } #[test] @@ -788,7 +848,10 @@ mod tests { transaction_v1_config: v1_config.clone(), ..Default::default() }; - assert!(validate_transaction_config(&transaction_config)); + assert!(validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); let mut definition_2 = definition_2.clone(); definition_2.set_max_transaction_gas_limit(definition_1.max_transaction_gas_limit()); v1_config.set_wasm_lanes(vec![ @@ -800,7 +863,10 @@ mod tests { transaction_v1_config: v1_config, ..Default::default() }; - assert!(!validate_transaction_config(&transaction_config)); + assert!(!validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); } #[test] @@ -834,7 +900,10 @@ mod tests { transaction_v1_config: v1_config.clone(), ..Default::default() }; - assert!(!validate_transaction_config(&transaction_config)); + assert!(!validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); } #[test] @@ -845,6 +914,139 @@ mod tests { transaction_v1_config: v1_config.clone(), ..Default::default() }; - assert!(!validate_transaction_config(&transaction_config)); + assert!(!validate_transaction_config( + &transaction_config, + &EvmConfig::default() + )); + } + + fn evm_config_with_lanes(enabled: bool, lanes: Vec) -> EvmConfig { + let mut evm_config = EvmConfig { + enabled, + ..Default::default() + }; + evm_config.set_transaction_lanes(lanes); + evm_config + } + + #[test] + fn should_pass_when_evm_disabled_and_lanes_empty() { + let evm_config = evm_config_with_lanes(false, vec![]); + assert!(validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_fail_when_evm_enabled_and_lanes_empty() { + let evm_config = evm_config_with_lanes(true, vec![]); + assert!(!validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_pass_with_valid_evm_lanes() { + let evm_config = evm_config_with_lanes( + true, + vec![ + TransactionLaneDefinition::new(100, 1000, 1000, 1_000_000, 10), + TransactionLaneDefinition::new(101, 2000, 2000, 2_000_000, 5), + ], + ); + assert!(validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_pass_with_evm_lane_id_below_100() { + // There is no numeric convention enforced on EVM lane ids: any id that doesn't + // collide with a reserved or wasm lane id is valid, including ids below 100. + let evm_config = evm_config_with_lanes( + true, + vec![TransactionLaneDefinition::new( + 10, 1000, 1000, 1_000_000, 10, + )], + ); + assert!(validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_fail_when_evm_lanes_have_duplicate_ids() { + let evm_config = evm_config_with_lanes( + true, + vec![ + TransactionLaneDefinition::new(100, 1000, 1000, 1_000_000, 10), + TransactionLaneDefinition::new(100, 2000, 2000, 2_000_000, 5), + ], + ); + assert!(!validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_fail_when_evm_lane_ids_collide_with_reserved() { + let evm_config = evm_config_with_lanes( + true, + vec![TransactionLaneDefinition::new( + MINT_LANE_ID, + 1000, + 1000, + 1_000_000, + 10, + )], + ); + assert!(!validate_evm_transaction_lanes( + &evm_config, + &TransactionV1Config::default() + )); + } + + #[test] + fn should_fail_when_evm_lane_ids_collide_with_wasm_lanes() { + let colliding_id = 150; + let mut v1_config = TransactionV1Config::default(); + v1_config.set_wasm_lanes(vec![TransactionLaneDefinition::new( + colliding_id, + 100, + 100, + 100, + 10, + )]); + + let non_colliding_evm_config = evm_config_with_lanes( + true, + vec![TransactionLaneDefinition::new( + 100, 1000, 1000, 1_000_000, 10, + )], + ); + assert!(validate_evm_transaction_lanes( + &non_colliding_evm_config, + &v1_config + )); + + let colliding_evm_config = evm_config_with_lanes( + true, + vec![TransactionLaneDefinition::new( + colliding_id, + 1000, + 1000, + 1_000_000, + 10, + )], + ); + assert!(!validate_evm_transaction_lanes( + &colliding_evm_config, + &v1_config + )); } } diff --git a/node/src/utils/chain_specification/parse_toml.rs b/node/src/utils/chain_specification/parse_toml.rs index 0e0c511e90..db47873d54 100644 --- a/node/src/utils/chain_specification/parse_toml.rs +++ b/node/src/utils/chain_specification/parse_toml.rs @@ -99,7 +99,7 @@ impl From<&Chainspec> for TomlChainspec { }; let core = chainspec.core_config.clone(); let transactions = chainspec.transaction_config.clone(); - let evm = chainspec.evm_config; + let evm = chainspec.evm_config.clone(); let highway = chainspec.highway_config; let wasm = chainspec.wasm_config; let system_costs = chainspec.system_costs_config; diff --git a/resources/integration-test/chainspec.toml b/resources/integration-test/chainspec.toml index a265c77594..6ef5abd259 100644 --- a/resources/integration-test/chainspec.toml +++ b/resources/integration-test/chainspec.toml @@ -523,3 +523,13 @@ base_fee = 5_000 # Number of wei represented by one mote. EVM gas prices are denominated in wei, # while Casper fee accounting is denominated in motes. wei_per_mote = 1_000_000_000 +# EVM transaction lanes, structured identically to [transactions.v1] wasm_lanes: +# [0] -> lane id (must be >= 100, unique among themselves and against all v1 lane ids) +# [1] -> max serialized length of the RLP-encoded EVM transaction, in bytes +# [2] -> max calldata (input) length, in bytes +# [3] -> max EVM gas limit for a transaction in this lane +# [4] -> max number of transactions this lane can hold in a block +transaction_lanes = [ + [100, 131_072, 1024, 1_000_000, 50], + [101, 262_144, 65_536, 15_000_000, 5], +] diff --git a/resources/local/chainspec.toml.in b/resources/local/chainspec.toml.in index 69f0081705..281abba48c 100644 --- a/resources/local/chainspec.toml.in +++ b/resources/local/chainspec.toml.in @@ -515,3 +515,13 @@ base_fee = 5_000 # Number of wei represented by one mote. EVM gas prices are denominated in wei, # while Casper fee accounting is denominated in motes. wei_per_mote = 1_000_000_000 +# EVM transaction lanes, structured identically to [transactions.v1] wasm_lanes: +# [0] -> lane id (must be >= 100, unique among themselves and against all v1 lane ids) +# [1] -> max serialized length of the RLP-encoded EVM transaction, in bytes +# [2] -> max calldata (input) length, in bytes +# [3] -> max EVM gas limit for a transaction in this lane +# [4] -> max number of transactions this lane can hold in a block +transaction_lanes = [ + [100, 131_072, 1024, 1_000_000, 50], + [101, 262_144, 65_536, 15_000_000, 5], +] diff --git a/resources/mainnet/chainspec.toml b/resources/mainnet/chainspec.toml index 49c0eaae93..411feebb3f 100644 --- a/resources/mainnet/chainspec.toml +++ b/resources/mainnet/chainspec.toml @@ -523,3 +523,13 @@ base_fee = 5_000 # Number of wei represented by one mote. EVM gas prices are denominated in wei, # while Casper fee accounting is denominated in motes. wei_per_mote = 1_000_000_000 +# EVM transaction lanes, structured identically to [transactions.v1] wasm_lanes: +# [0] -> lane id (must be >= 100, unique among themselves and against all v1 lane ids) +# [1] -> max serialized length of the RLP-encoded EVM transaction, in bytes +# [2] -> max calldata (input) length, in bytes +# [3] -> max EVM gas limit for a transaction in this lane +# [4] -> max number of transactions this lane can hold in a block +transaction_lanes = [ + [100, 131_072, 1024, 1_000_000, 50], + [101, 262_144, 65_536, 15_000_000, 5], +] diff --git a/resources/production/chainspec.toml b/resources/production/chainspec.toml index 3b6802ffbf..5ed5293963 100644 --- a/resources/production/chainspec.toml +++ b/resources/production/chainspec.toml @@ -522,3 +522,13 @@ base_fee = 5_000 # Number of wei represented by one mote. EVM gas prices are denominated in wei, # while Casper fee accounting is denominated in motes. wei_per_mote = 1_000_000_000 +# EVM transaction lanes, structured identically to [transactions.v1] wasm_lanes: +# [0] -> lane id (must be >= 100, unique among themselves and against all v1 lane ids) +# [1] -> max serialized length of the RLP-encoded EVM transaction, in bytes +# [2] -> max calldata (input) length, in bytes +# [3] -> max EVM gas limit for a transaction in this lane +# [4] -> max number of transactions this lane can hold in a block +transaction_lanes = [ + [100, 131_072, 1024, 1_000_000, 50], + [101, 262_144, 65_536, 15_000_000, 5], +] diff --git a/resources/testnet/chainspec.toml b/resources/testnet/chainspec.toml index 6f31f153ed..67c2ceb40f 100644 --- a/resources/testnet/chainspec.toml +++ b/resources/testnet/chainspec.toml @@ -525,3 +525,13 @@ base_fee = 5_000 # Number of wei represented by one mote. EVM gas prices are denominated in wei, # while Casper fee accounting is denominated in motes. wei_per_mote = 1_000_000_000 +# EVM transaction lanes, structured identically to [transactions.v1] wasm_lanes: +# [0] -> lane id (must be >= 100, unique among themselves and against all v1 lane ids) +# [1] -> max serialized length of the RLP-encoded EVM transaction, in bytes +# [2] -> max calldata (input) length, in bytes +# [3] -> max EVM gas limit for a transaction in this lane +# [4] -> max number of transactions this lane can hold in a block +transaction_lanes = [ + [100, 131_072, 1024, 1_000_000, 50], + [101, 262_144, 65_536, 15_000_000, 5], +] diff --git a/types/src/block.rs b/types/src/block.rs index 9daa3ba1f4..f9913be98a 100644 --- a/types/src/block.rs +++ b/types/src/block.rs @@ -38,7 +38,7 @@ use num_rational::Ratio; use schemars::JsonSchema; #[cfg(feature = "std")] -use crate::TransactionConfig; +use crate::{EvmConfig, TransactionConfig}; use crate::{ bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, @@ -421,8 +421,10 @@ impl Block { pub fn block_utilization( &self, transaction_config_input: impl Borrow, + evm_config_input: impl Borrow, ) -> u64 { let transaction_config = transaction_config_input.borrow(); + let evm_config = evm_config_input.borrow(); match self { Block::V1(_) => { // We shouldnt be tracking this for legacy blocks @@ -431,8 +433,9 @@ impl Block { Block::V2(block_v2) => { let per_block_capacity = transaction_config .transaction_v1_config - .get_max_block_count(); - let has_hit_slot_limt = self.has_hit_slot_capacity(transaction_config); + .get_max_block_count() + + evm_config.get_max_evm_transaction_count().unwrap_or(0); + let has_hit_slot_limt = self.has_hit_slot_capacity(transaction_config, evm_config); if has_hit_slot_limt { 100u64 } else { @@ -448,8 +451,10 @@ impl Block { pub fn has_hit_slot_capacity( &self, transaction_config_input: impl Borrow, + evm_config_input: impl Borrow, ) -> bool { let transaction_config = transaction_config_input.borrow(); + let evm_config = evm_config_input.borrow(); match self { Block::V1(_) => false, Block::V2(block_v2) => { @@ -488,9 +493,13 @@ impl Block { if *lane_id < 2 { continue; }; - let max_transaction_count = transaction_config - .transaction_v1_config - .get_max_transaction_count(*lane_id); + let max_transaction_count = if evm_config.is_supported(*lane_id) { + evm_config.get_max_transaction_count(*lane_id) + } else { + transaction_config + .transaction_v1_config + .get_max_transaction_count(*lane_id) + }; if transaction_count as u64 >= max_transaction_count { return true; diff --git a/types/src/chainspec.rs b/types/src/chainspec.rs index 2da7b1ba22..ddf839ee65 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -5,6 +5,7 @@ mod accounts_config; mod activation_point; mod chainspec_raw_bytes; mod core_config; +mod evm; mod fee_handling; pub mod genesis_config; mod global_state_update; @@ -35,7 +36,7 @@ use tracing::error; use crate::testing::TestRng; use crate::{ bytesrepr::{self, FromBytes, ToBytes}, - ChainNameDigest, Digest, EraId, EvmConfig, ProtocolVersion, Timestamp, + ChainNameDigest, Digest, EraId, ProtocolVersion, Timestamp, }; pub use accounts_config::{ AccountConfig, AccountsConfig, AdministratorAccount, DelegatorConfig, GenesisAccount, @@ -52,6 +53,7 @@ pub use core_config::{ pub use core_config::{ DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_FEE_HANDLING, DEFAULT_REFUND_HANDLING, }; +pub use evm::{EvmConfig, EvmSpec, DEFAULT_WEI_PER_MOTE, MINIMUM_WEI_PER_MOTE}; pub use fee_handling::FeeHandling; #[cfg(any(feature = "std", test))] pub use genesis_config::GenesisConfig; @@ -218,7 +220,7 @@ impl Chainspec { Some(self.core_config.unbonding_delay), global_state_update, chainspec_registry, - self.evm_config, + self.evm_config.clone(), fee_handling, validator_minimum_bid_amount, maximum_delegation_amount, @@ -239,6 +241,9 @@ impl Chainspec { /// Is the given transaction lane supported. pub fn is_supported(&self, lane: u8) -> bool { + if self.evm_config.is_supported(lane) { + return true; + } self.transaction_config .transaction_v1_config .is_supported(lane) @@ -246,6 +251,9 @@ impl Chainspec { /// Returns the max serialized for the given category. pub fn get_max_serialized_length_by_category(&self, lane: u8) -> u64 { + if self.evm_config.is_supported(lane) { + return self.evm_config.get_max_serialized_length(lane); + } self.transaction_config .transaction_v1_config .get_max_serialized_length(lane) @@ -253,6 +261,9 @@ impl Chainspec { /// Returns the max args length for the given category. pub fn get_max_args_length_by_category(&self, lane: u8) -> u64 { + if self.evm_config.is_supported(lane) { + return self.evm_config.get_max_args_length(lane); + } self.transaction_config .transaction_v1_config .get_max_args_length(lane) @@ -260,6 +271,9 @@ impl Chainspec { /// Returns the max gas limit for the given category. pub fn get_max_gas_limit_by_category(&self, lane: u8) -> u64 { + if self.evm_config.is_supported(lane) { + return self.evm_config.get_max_transaction_gas_limit(lane); + } self.transaction_config .transaction_v1_config .get_max_transaction_gas_limit(lane) @@ -267,6 +281,9 @@ impl Chainspec { /// Returns the max transaction count for the given category. pub fn get_max_transaction_count_by_category(&self, lane: u8) -> u64 { + if self.evm_config.is_supported(lane) { + return self.evm_config.get_max_transaction_count(lane); + } self.transaction_config .transaction_v1_config .get_max_transaction_count(lane) @@ -412,4 +429,57 @@ mod tests { let chainspec = Chainspec::random(&mut rng); bytesrepr::test_serialization_roundtrip(&chainspec); } + + #[test] + fn should_route_lane_lookups_to_v1_when_not_an_evm_lane() { + const EVM_LANE_ID: u8 = 100; + let mut chainspec = Chainspec::default(); + chainspec + .evm_config + .set_transaction_lanes(vec![crate::TransactionLaneDefinition::new( + EVM_LANE_ID, + 111, + 222, + 333, + 444, + )]); + let wasm_lane_id = chainspec + .transaction_config + .transaction_v1_config + .wasm_lanes()[0] + .id(); + + assert!(chainspec.is_supported(wasm_lane_id)); + // Not the configured EVM lane id, and not a v1 lane id either: unsupported by either. + assert!(!chainspec.is_supported(EVM_LANE_ID - 1)); + } + + #[test] + fn should_route_lane_lookups_to_evm_based_on_configured_lane_ids() { + // Deliberately use a low id to prove routing is decided by membership in + // `evm_config.transaction_lanes()`, not by any numeric threshold. + const EVM_LANE_ID: u8 = 10; + let mut chainspec = Chainspec::default(); + chainspec + .evm_config + .set_transaction_lanes(vec![crate::TransactionLaneDefinition::new( + EVM_LANE_ID, + 111, + 222, + 333, + 444, + )]); + + assert!(chainspec.is_supported(EVM_LANE_ID)); + assert_eq!( + chainspec.get_max_serialized_length_by_category(EVM_LANE_ID), + 111 + ); + assert_eq!(chainspec.get_max_args_length_by_category(EVM_LANE_ID), 222); + assert_eq!(chainspec.get_max_gas_limit_by_category(EVM_LANE_ID), 333); + assert_eq!( + chainspec.get_max_transaction_count_by_category(EVM_LANE_ID), + 444 + ); + } } diff --git a/types/src/chainspec/evm.rs b/types/src/chainspec/evm.rs new file mode 100644 index 0000000000..b62de896fa --- /dev/null +++ b/types/src/chainspec/evm.rs @@ -0,0 +1,495 @@ +use alloc::vec::Vec; + +#[cfg(feature = "datasize")] +use datasize::DataSize; +#[cfg(feature = "json-schema")] +use schemars::JsonSchema; +#[cfg(any(feature = "std", test))] +use serde::{ + de::{Error as DeError, Unexpected}, + Deserializer, Serializer, +}; +use serde::{Deserialize, Serialize}; + +use super::TransactionLaneDefinition; +use crate::{ + bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, + EvmFeeConfig, U256, U512, +}; + +/// The default number of wei represented by one mote. +pub const DEFAULT_WEI_PER_MOTE: u64 = 1_000_000_000; + +/// The minimum number of wei represented by one mote. +pub const MINIMUM_WEI_PER_MOTE: u64 = DEFAULT_WEI_PER_MOTE; + +/// Supported EVM hardfork specifications for chainspec configuration. +/// +/// Variants are ordered by fork chronology; feature gates use ordinal +/// comparisons to apply functionality from a fork onward. +#[derive( + Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, +)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(rename_all = "snake_case")] +pub enum EvmSpec { + /// Prague. + #[default] + Prague, +} + +impl EvmSpec { + fn tag(self) -> u8 { + match self { + EvmSpec::Prague => 0, + } + } +} + +impl ToBytes for EvmSpec { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Ok(vec![self.tag()]) + } + + fn serialized_length(&self) -> usize { + U8_SERIALIZED_LENGTH + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + writer.push(self.tag()); + Ok(()) + } +} + +impl FromBytes for EvmSpec { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (tag, remainder) = u8::from_bytes(bytes)?; + let spec = match tag { + 0 => EvmSpec::Prague, + _ => return Err(bytesrepr::Error::Formatting), + }; + Ok((spec, remainder)) + } +} + +/// Chainspec configuration for EVM execution. +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(JsonSchema))] +#[serde(deny_unknown_fields)] +pub struct EvmConfig { + /// Whether EVM execution is enabled. + pub enabled: bool, + /// EVM chain ID used for the `CHAINID` opcode and transaction validation. + pub chain_id: u64, + /// Hardfork specification used by the EVM executor. + pub spec: EvmSpec, + /// Per-block gas limit supplied to the EVM block context. + pub block_gas_limit: u64, + /// Base fee denominated in motes per EVM gas. + pub base_fee: u64, + /// Number of wei represented by one mote. + pub wei_per_mote: u64, + /// Lane configurations for EVM transactions. + #[serde( + serialize_with = "transaction_lane_definitions_to_vec", + deserialize_with = "vec_to_transaction_lane_definitions" + )] + pub transaction_lanes: Vec, +} + +impl Default for EvmConfig { + fn default() -> Self { + EvmConfig { + enabled: false, + chain_id: 0, + spec: EvmSpec::Prague, + block_gas_limit: 30_000_000, + base_fee: 0, + wei_per_mote: DEFAULT_WEI_PER_MOTE, + transaction_lanes: Vec::new(), + } + } +} + +#[cfg(any(feature = "std", test))] +fn transaction_lane_definition_to_vec(lane: &TransactionLaneDefinition) -> Vec { + vec![ + lane.id() as u64, + lane.max_transaction_length(), + lane.max_transaction_args_length(), + lane.max_transaction_gas_limit(), + lane.max_transaction_count(), + ] +} + +#[cfg(any(feature = "std", test))] +fn transaction_lane_definitions_to_vec( + lanes: &[TransactionLaneDefinition], + serializer: S, +) -> Result +where + S: Serializer, +{ + let as_vecs: Vec> = lanes + .iter() + .map(transaction_lane_definition_to_vec) + .collect(); + as_vecs.serialize(serializer) +} + +#[cfg(any(feature = "std", test))] +fn vec_to_transaction_lane_definitions<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let raw_lanes = Vec::>::deserialize(deserializer)?; + raw_lanes + .into_iter() + .map(|v| { + TransactionLaneDefinition::try_from(v).map_err(|_| { + DeError::invalid_value( + Unexpected::Seq, + &"expected 5 u64 compliant numbers to create a TransactionLaneDefinition", + ) + }) + }) + .collect() +} + +impl EvmConfig { + /// Returns the subset of this configuration needed to price EVM transactions. + pub fn fee_config(&self) -> EvmFeeConfig { + EvmFeeConfig::new(self.base_fee, self.wei_per_mote) + } + + /// Returns the EVM base fee denominated in wei. + pub fn base_fee_wei(&self) -> u128 { + self.fee_config().base_fee_wei() + } + + /// Converts an EVM gas cost, denominated in wei, to motes by rounding up. + /// + /// See [`EvmFeeConfig::gas_fee_motes`]. + pub fn gas_fee_motes(&self, gas: u64, gas_price_wei: u128) -> Option { + self.fee_config().gas_fee_motes(gas, gas_price_wei) + } + + /// Converts an Ethereum transaction value from wei to motes. + /// + /// See [`EvmFeeConfig::value_motes`]. + pub fn value_motes(&self, value_wei: U256) -> Option { + self.fee_config().value_motes(value_wei) + } +} + +// EVM transaction lanes reuse `TransactionLaneDefinition`, which lives in the `chainspec`. +impl EvmConfig { + /// Returns the configured EVM transaction lanes. + pub fn transaction_lanes(&self) -> &Vec { + &self.transaction_lanes + } + + /// Sets the configured EVM transaction lanes. + #[cfg(any(feature = "testing", test))] + pub fn set_transaction_lanes(&mut self, transaction_lanes: Vec) { + self.transaction_lanes = transaction_lanes; + } + + /// Returns the lane definition matching the given lane id, if any. + pub fn get_lane_by_id(&self, lane_id: u8) -> Option<&TransactionLaneDefinition> { + self.transaction_lanes + .iter() + .find(|lane| lane.id() == lane_id) + } + + /// Returns the max serialized length of a transaction for the given EVM lane. + pub fn get_max_serialized_length(&self, lane_id: u8) -> u64 { + self.get_lane_by_id(lane_id) + .map(TransactionLaneDefinition::max_transaction_length) + .unwrap_or(0) + } + + /// Returns the max calldata (args) length of a transaction for the given EVM lane. + pub fn get_max_args_length(&self, lane_id: u8) -> u64 { + self.get_lane_by_id(lane_id) + .map(TransactionLaneDefinition::max_transaction_args_length) + .unwrap_or(0) + } + + /// Returns the max gas limit of a transaction for the given EVM lane. + pub fn get_max_transaction_gas_limit(&self, lane_id: u8) -> u64 { + self.get_lane_by_id(lane_id) + .map(TransactionLaneDefinition::max_transaction_gas_limit) + .unwrap_or(0) + } + + /// Returns the max transaction count for the given EVM lane. + pub fn get_max_transaction_count(&self, lane_id: u8) -> u64 { + self.get_lane_by_id(lane_id) + .map(TransactionLaneDefinition::max_transaction_count) + .unwrap_or(0) + } + + /// Returns the maximum number of EVM transactions across all configured EVM lanes. + pub fn get_max_evm_transaction_count(&self) -> Option { + if !self.enabled { + return None; + } + Some( + self.transaction_lanes + .iter() + .map(TransactionLaneDefinition::max_transaction_count) + .sum(), + ) + } + + /// Is the given EVM lane identifier supported. + pub fn is_supported(&self, lane_id: u8) -> bool { + self.transaction_lanes + .iter() + .any(|lane| lane.id() == lane_id) + } + + /// Returns the list of currently supported EVM lane identifiers. + pub fn get_supported_lanes(&self) -> Vec { + self.transaction_lanes + .iter() + .map(TransactionLaneDefinition::id) + .collect() + } + + /// Returns the smallest EVM lane id whose limits can accommodate a transaction with the + /// given gas limit, serialized size and calldata (input) size. Lanes are considered in + /// ascending order of `(max_transaction_gas_limit, max_transaction_length, + /// max_transaction_args_length, id)`, mirroring + /// `TransactionV1Config::get_wasm_lane_id_by_payment_limited`. + pub fn get_evm_lane_id( + &self, + gas_limit: u64, + transaction_size: u64, + input_size: u64, + ) -> Option { + let mut lanes: Vec<&TransactionLaneDefinition> = self.transaction_lanes.iter().collect(); + lanes.sort_by_key(|lane| { + ( + lane.max_transaction_gas_limit(), + lane.max_transaction_length(), + lane.max_transaction_args_length(), + lane.id(), + ) + }); + lanes + .into_iter() + .find(|lane| { + gas_limit <= lane.max_transaction_gas_limit() + && transaction_size <= lane.max_transaction_length() + && input_size <= lane.max_transaction_args_length() + }) + .map(TransactionLaneDefinition::id) + } +} + +impl ToBytes for EvmConfig { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + let mut buffer = bytesrepr::allocate_buffer(self)?; + self.write_bytes(&mut buffer)?; + Ok(buffer) + } + + fn serialized_length(&self) -> usize { + let base = self.enabled.serialized_length() + + self.chain_id.serialized_length() + + self.spec.serialized_length() + + self.block_gas_limit.serialized_length() + + self.base_fee.serialized_length() + + self.wei_per_mote.serialized_length(); + let base = { + let transaction_lanes_as_vecs: Vec> = self + .transaction_lanes + .iter() + .map(transaction_lane_definition_to_vec) + .collect(); + base + transaction_lanes_as_vecs.serialized_length() + }; + base + } + + fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { + self.enabled.write_bytes(writer)?; + self.chain_id.write_bytes(writer)?; + self.spec.write_bytes(writer)?; + self.block_gas_limit.write_bytes(writer)?; + self.base_fee.write_bytes(writer)?; + self.wei_per_mote.write_bytes(writer)?; + let transaction_lanes_as_vecs: Vec> = self + .transaction_lanes + .iter() + .map(transaction_lane_definition_to_vec) + .collect(); + transaction_lanes_as_vecs.write_bytes(writer)?; + Ok(()) + } +} + +impl FromBytes for EvmConfig { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (enabled, remainder) = bool::from_bytes(bytes)?; + let (chain_id, remainder) = u64::from_bytes(remainder)?; + let (spec, remainder) = EvmSpec::from_bytes(remainder)?; + let (block_gas_limit, remainder) = u64::from_bytes(remainder)?; + let (base_fee, remainder) = u64::from_bytes(remainder)?; + let (wei_per_mote, remainder) = u64::from_bytes(remainder)?; + let (raw_transaction_lanes, remainder): (Vec>, &[u8]) = + FromBytes::from_bytes(remainder)?; + let transaction_lanes: Result, _> = raw_transaction_lanes + .into_iter() + .map(TransactionLaneDefinition::try_from) + .collect(); + Ok(( + EvmConfig { + enabled, + chain_id, + spec, + block_gas_limit, + base_fee, + wei_per_mote, + transaction_lanes: transaction_lanes.map_err(|_| bytesrepr::Error::Formatting)?, + }, + remainder, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_scale_base_fee_to_wei() { + let config = EvmConfig { + base_fee: 3, + ..Default::default() + }; + + assert_eq!(config.base_fee_wei(), 3_000_000_000u128); + } + + #[test] + fn should_convert_wei_gas_fee_to_motes() { + let config = EvmConfig::default(); + + assert_eq!( + config.gas_fee_motes(21_000, u128::from(DEFAULT_WEI_PER_MOTE)), + Some(U512::from(21_000)) + ); + } + + #[test] + fn should_round_sub_mote_wei_gas_fee_up_to_one_mote() { + let config = EvmConfig::default(); + + assert_eq!(config.gas_fee_motes(1, 1), Some(U512::from(1))); + } + + #[test] + fn should_convert_exact_evm_value_from_wei_to_motes() { + let config = EvmConfig::default(); + + assert_eq!( + config.value_motes(U256::from(DEFAULT_WEI_PER_MOTE) * U256::from(15u64)), + Some(U256::from(15u64)) + ); + } + + #[test] + fn should_reject_evm_value_with_fractional_mote() { + let config = EvmConfig::default(); + + assert_eq!(config.value_motes(U256::from(1u64)), None); + } + + fn config_with_lanes() -> EvmConfig { + let mut config = EvmConfig::default(); + config.enabled = true; + config.set_transaction_lanes(vec![ + TransactionLaneDefinition::new(100, 1_000, 100, 1_000_000, 5), + TransactionLaneDefinition::new(101, 10_000, 1_000, 10_000_000, 2), + ]); + config + } + + #[test] + fn should_pick_smallest_fitting_lane() { + let config = config_with_lanes(); + assert_eq!(config.get_evm_lane_id(500, 500, 50), Some(100)); + } + + #[test] + fn should_take_gas_limit_into_account() { + let config = config_with_lanes(); + // Small enough for lane 100 by size, but gas limit only fits lane 101. + assert_eq!(config.get_evm_lane_id(2_000_000, 500, 50), Some(101)); + } + + #[test] + fn should_take_calldata_size_into_account() { + let config = config_with_lanes(); + // Small enough for lane 100 by size and gas, but calldata only fits lane 101. + assert_eq!(config.get_evm_lane_id(500, 500, 500), Some(101)); + } + + #[test] + fn should_return_none_when_no_lane_fits() { + let config = config_with_lanes(); + assert_eq!(config.get_evm_lane_id(u64::MAX, 500, 50), None); + } + + #[test] + fn should_return_none_when_no_lanes_configured() { + let config = EvmConfig::default(); + assert_eq!(config.get_evm_lane_id(1, 1, 1), None); + } + + #[test] + fn should_report_supported_lanes() { + let config = config_with_lanes(); + assert!(config.is_supported(100)); + assert!(config.is_supported(101)); + assert!(!config.is_supported(102)); + let mut supported = config.get_supported_lanes(); + supported.sort_unstable(); + assert_eq!(supported, vec![100, 101]); + } + + #[test] + fn should_sum_max_evm_transaction_count() { + let config = config_with_lanes(); + assert_eq!(config.get_max_evm_transaction_count(), Some(7)); + } + + #[test] + fn should_return_none_max_evm_transaction_count_when_evm_disabled() { + let mut config = config_with_lanes(); + config.enabled = false; + assert_eq!(config.get_max_evm_transaction_count(), None); + } + + #[test] + fn should_bytesrepr_roundtrip_with_transaction_lanes() { + let config = config_with_lanes(); + bytesrepr::test_serialization_roundtrip(&config); + } + + #[test] + fn should_serde_roundtrip_with_transaction_lanes() { + let config = config_with_lanes(); + let serialized = serde_json::to_string(&config).expect("should serialize"); + let deserialized: EvmConfig = + serde_json::from_str(&serialized).expect("should deserialize"); + assert_eq!(config, deserialized); + } +} diff --git a/types/src/chainspec/genesis_config.rs b/types/src/chainspec/genesis_config.rs index 73e34ea2ec..5676f05935 100644 --- a/types/src/chainspec/genesis_config.rs +++ b/types/src/chainspec/genesis_config.rs @@ -279,7 +279,7 @@ impl From<&Chainspec> for GenesisConfig { let storage_costs = chainspec.storage_costs; GenesisConfig { accounts: chainspec.network_config.accounts_config.clone().into(), - evm_config: chainspec.evm_config, + evm_config: chainspec.evm_config.clone(), wasm_config: chainspec.wasm_config, system_config: chainspec.system_costs_config, validator_slots: chainspec.core_config.validator_slots, diff --git a/types/src/chainspec/transaction_config/transaction_v1_config.rs b/types/src/chainspec/transaction_config/transaction_v1_config.rs index ecd0753236..2ac457dce4 100644 --- a/types/src/chainspec/transaction_config/transaction_v1_config.rs +++ b/types/src/chainspec/transaction_config/transaction_v1_config.rs @@ -35,6 +35,7 @@ const TRANSACTION_COUNT_INDEX: usize = 4; /// Structured limits imposed on a transaction lane #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] #[cfg_attr(feature = "datasize", derive(DataSize))] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] pub struct TransactionLaneDefinition { /// The lane identifier #[deprecated( diff --git a/types/src/evm.rs b/types/src/evm.rs index 7de738e962..06827f00de 100644 --- a/types/src/evm.rs +++ b/types/src/evm.rs @@ -7,8 +7,8 @@ mod account; mod address; -mod config; mod evm_addr; +mod fee_config; mod hash; mod receipt; mod topic; @@ -28,8 +28,8 @@ pub use transaction::{ // (`casper_types::EvmFoo`), not through `casper_types::evm::EvmFoo`. // They are re-exported here so the rest of `casper-types` can import them // without going through the crate root. -pub use config::{EvmConfig, EvmSpec, DEFAULT_WEI_PER_MOTE, MINIMUM_WEI_PER_MOTE}; pub use evm_addr::EvmAddr; +pub use fee_config::EvmFeeConfig; pub use transaction::{ EvmApproval, EvmTransaction, EvmTransactionError, EvmTransactionHash, EvmTransactionKind, }; diff --git a/types/src/evm/config.rs b/types/src/evm/config.rs deleted file mode 100644 index 6a74531a26..0000000000 --- a/types/src/evm/config.rs +++ /dev/null @@ -1,237 +0,0 @@ -use alloc::vec::Vec; - -#[cfg(feature = "datasize")] -use datasize::DataSize; -use num_rational::Ratio; -#[cfg(feature = "json-schema")] -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{ - bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, - U256, U512, -}; - -/// The default number of wei represented by one mote. -pub const DEFAULT_WEI_PER_MOTE: u64 = 1_000_000_000; - -/// The minimum number of wei represented by one mote. -pub const MINIMUM_WEI_PER_MOTE: u64 = DEFAULT_WEI_PER_MOTE; - -/// Supported EVM hardfork specifications for chainspec configuration. -/// -/// Variants are ordered by fork chronology; feature gates use ordinal -/// comparisons to apply functionality from a fork onward. -#[derive( - Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize, -)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[serde(rename_all = "snake_case")] -pub enum EvmSpec { - /// Prague. - #[default] - Prague, -} - -impl EvmSpec { - fn tag(self) -> u8 { - match self { - EvmSpec::Prague => 0, - } - } -} - -impl ToBytes for EvmSpec { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - Ok(vec![self.tag()]) - } - - fn serialized_length(&self) -> usize { - U8_SERIALIZED_LENGTH - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - writer.push(self.tag()); - Ok(()) - } -} - -impl FromBytes for EvmSpec { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (tag, remainder) = u8::from_bytes(bytes)?; - let spec = match tag { - 0 => EvmSpec::Prague, - _ => return Err(bytesrepr::Error::Formatting), - }; - Ok((spec, remainder)) - } -} - -/// Chainspec configuration for EVM execution. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "datasize", derive(DataSize))] -#[cfg_attr(feature = "json-schema", derive(JsonSchema))] -#[serde(deny_unknown_fields)] -pub struct EvmConfig { - /// Whether EVM execution is enabled. - pub enabled: bool, - /// EVM chain ID used for the `CHAINID` opcode and transaction validation. - pub chain_id: u64, - /// Hardfork specification used by the EVM executor. - pub spec: EvmSpec, - /// Per-block gas limit supplied to the EVM block context. - pub block_gas_limit: u64, - /// Base fee denominated in motes per EVM gas. - pub base_fee: u64, - /// Number of wei represented by one mote. - pub wei_per_mote: u64, -} - -impl Default for EvmConfig { - fn default() -> Self { - EvmConfig { - enabled: false, - chain_id: 0, - spec: EvmSpec::Prague, - block_gas_limit: 30_000_000, - base_fee: 0, - wei_per_mote: DEFAULT_WEI_PER_MOTE, - } - } -} - -impl EvmConfig { - /// Returns the EVM base fee denominated in wei. - pub fn base_fee_wei(&self) -> u128 { - u128::from(self.base_fee) * u128::from(self.wei_per_mote) - } - - /// Converts an EVM gas cost, denominated in wei, to motes by rounding up. - /// - /// Rounding is applied after multiplying gas by price, so sub-mote totals - /// are charged as one mote without overcharging each gas unit separately. - pub fn gas_fee_motes(&self, gas: u64, gas_price_wei: u128) -> Option { - if self.wei_per_mote == 0 { - return None; - } - let fee_wei = U512::from(gas).checked_mul(U512::from(gas_price_wei))?; - Some( - Ratio::new(fee_wei, U512::from(self.wei_per_mote)) - .ceil() - .to_integer(), - ) - } - - /// Converts an Ethereum transaction value from wei to motes. - /// - /// Casper purse balances have mote precision, so values containing a - /// fractional mote are not representable and return `None`. - pub fn value_motes(&self, value_wei: U256) -> Option { - if self.wei_per_mote == 0 { - return None; - } - let wei_per_mote = U256::from(self.wei_per_mote); - if value_wei % wei_per_mote != U256::zero() { - return None; - } - Some(value_wei / wei_per_mote) - } -} - -impl ToBytes for EvmConfig { - fn to_bytes(&self) -> Result, bytesrepr::Error> { - let mut buffer = bytesrepr::allocate_buffer(self)?; - self.write_bytes(&mut buffer)?; - Ok(buffer) - } - - fn serialized_length(&self) -> usize { - self.enabled.serialized_length() - + self.chain_id.serialized_length() - + self.spec.serialized_length() - + self.block_gas_limit.serialized_length() - + self.base_fee.serialized_length() - + self.wei_per_mote.serialized_length() - } - - fn write_bytes(&self, writer: &mut Vec) -> Result<(), bytesrepr::Error> { - self.enabled.write_bytes(writer)?; - self.chain_id.write_bytes(writer)?; - self.spec.write_bytes(writer)?; - self.block_gas_limit.write_bytes(writer)?; - self.base_fee.write_bytes(writer)?; - self.wei_per_mote.write_bytes(writer) - } -} - -impl FromBytes for EvmConfig { - fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { - let (enabled, remainder) = bool::from_bytes(bytes)?; - let (chain_id, remainder) = u64::from_bytes(remainder)?; - let (spec, remainder) = EvmSpec::from_bytes(remainder)?; - let (block_gas_limit, remainder) = u64::from_bytes(remainder)?; - let (base_fee, remainder) = u64::from_bytes(remainder)?; - let (wei_per_mote, remainder) = u64::from_bytes(remainder)?; - Ok(( - EvmConfig { - enabled, - chain_id, - spec, - block_gas_limit, - base_fee, - wei_per_mote, - }, - remainder, - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn should_scale_base_fee_to_wei() { - let config = EvmConfig { - base_fee: 3, - ..Default::default() - }; - - assert_eq!(config.base_fee_wei(), 3_000_000_000u128); - } - - #[test] - fn should_convert_wei_gas_fee_to_motes() { - let config = EvmConfig::default(); - - assert_eq!( - config.gas_fee_motes(21_000, u128::from(DEFAULT_WEI_PER_MOTE)), - Some(U512::from(21_000)) - ); - } - - #[test] - fn should_round_sub_mote_wei_gas_fee_up_to_one_mote() { - let config = EvmConfig::default(); - - assert_eq!(config.gas_fee_motes(1, 1), Some(U512::from(1))); - } - - #[test] - fn should_convert_exact_evm_value_from_wei_to_motes() { - let config = EvmConfig::default(); - - assert_eq!( - config.value_motes(U256::from(DEFAULT_WEI_PER_MOTE) * U256::from(15u64)), - Some(U256::from(15u64)) - ); - } - - #[test] - fn should_reject_evm_value_with_fractional_mote() { - let config = EvmConfig::default(); - - assert_eq!(config.value_motes(U256::from(1u64)), None); - } -} diff --git a/types/src/evm/fee_config.rs b/types/src/evm/fee_config.rs new file mode 100644 index 0000000000..937cf42d0b --- /dev/null +++ b/types/src/evm/fee_config.rs @@ -0,0 +1,68 @@ +use num_rational::Ratio; + +use crate::{U256, U512}; + +/// The subset of the EVM chainspec configuration needed to price EVM transactions. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct EvmFeeConfig { + /// Base fee denominated in motes per EVM gas. + base_fee: u64, + /// Number of wei represented by one mote. + wei_per_mote: u64, +} + +impl EvmFeeConfig { + /// Creates a new `EvmFeeConfig`. + pub fn new(base_fee: u64, wei_per_mote: u64) -> Self { + EvmFeeConfig { + base_fee, + wei_per_mote, + } + } + + /// Returns the base fee denominated in motes per EVM gas. + pub fn base_fee(&self) -> u64 { + self.base_fee + } + + /// Returns the number of wei represented by one mote. + pub fn wei_per_mote(&self) -> u64 { + self.wei_per_mote + } + + /// Returns the EVM base fee denominated in wei. + pub fn base_fee_wei(&self) -> u128 { + u128::from(self.base_fee) * u128::from(self.wei_per_mote) + } + + /// Converts an EVM gas cost, denominated in wei, to motes by rounding up. + /// + /// Rounding is applied after multiplying gas by price, so sub-mote totals + /// are charged as one mote without overcharging each gas unit separately. + pub fn gas_fee_motes(&self, gas: u64, gas_price_wei: u128) -> Option { + if self.wei_per_mote == 0 { + return None; + } + let fee_wei = U512::from(gas).checked_mul(U512::from(gas_price_wei))?; + Some( + Ratio::new(fee_wei, U512::from(self.wei_per_mote)) + .ceil() + .to_integer(), + ) + } + + /// Converts an Ethereum transaction value from wei to motes. + /// + /// Casper purse balances have mote precision, so values containing a + /// fractional mote are not representable and return `None`. + pub fn value_motes(&self, value_wei: U256) -> Option { + if self.wei_per_mote == 0 { + return None; + } + let wei_per_mote = U256::from(self.wei_per_mote); + if value_wei % wei_per_mote != U256::zero() { + return None; + } + Some(value_wei / wei_per_mote) + } +} diff --git a/types/src/evm/transaction.rs b/types/src/evm/transaction.rs index 0df1129623..33042bfa6e 100644 --- a/types/src/evm/transaction.rs +++ b/types/src/evm/transaction.rs @@ -33,7 +33,7 @@ use schemars::JsonSchema; use serde::{de, Deserializer, Serializer}; use serde::{Deserialize, Serialize}; -use super::{Address, EvmConfig, Hash, HASH_LENGTH}; +use super::{Address, EvmFeeConfig, Hash, HASH_LENGTH}; #[cfg(any(feature = "testing", test))] use crate::testing::TestRng; use crate::{ @@ -1248,19 +1248,19 @@ impl EvmTransaction { } /// Returns the fee amount for `gas_used`, denominated in motes. - pub fn fee_amount(&self, gas_used: u64, evm_config: &EvmConfig) -> Option { - let gas_price_wei = self.effective_gas_price(evm_config.base_fee_wei()); - evm_config.gas_fee_motes(gas_used, gas_price_wei) + pub fn fee_amount(&self, gas_used: u64, fee_config: &EvmFeeConfig) -> Option { + let gas_price_wei = self.effective_gas_price(fee_config.base_fee_wei()); + fee_config.gas_fee_motes(gas_used, gas_price_wei) } /// Returns the maximum signed fee amount that must be reserved, denominated in motes. - pub fn max_fee_amount(&self, evm_config: &EvmConfig) -> Option { - evm_config.gas_fee_motes(self.gas_limit, self.maximum_fee_per_gas()) + pub fn max_fee_amount(&self, fee_config: &EvmFeeConfig) -> Option { + fee_config.gas_fee_motes(self.gas_limit, self.maximum_fee_per_gas()) } /// Returns the balance needed for value transfer plus the supplied fee amount, in motes. - pub fn required_balance(&self, fee_amount: U512, evm_config: &EvmConfig) -> Option { - let value_motes = evm_config.value_motes(self.value)?; + pub fn required_balance(&self, fee_amount: U512, fee_config: &EvmFeeConfig) -> Option { + let value_motes = fee_config.value_motes(self.value)?; fee_amount.checked_add(U512::from(value_motes)) } @@ -1853,12 +1853,7 @@ mod tests { #[test] fn dynamic_fee_reserves_signed_maximum_but_charges_effective_price() { let transaction = signed_eip7702_transaction(); - let config = EvmConfig { - enabled: true, - base_fee: 1, - wei_per_mote: crate::DEFAULT_WEI_PER_MOTE, - ..Default::default() - }; + let config = EvmFeeConfig::new(1, crate::DEFAULT_WEI_PER_MOTE); assert_eq!( transaction.maximum_fee_per_gas(), diff --git a/types/src/lib.rs b/types/src/lib.rs index aaa41dac1f..1f368c39df 100644 --- a/types/src/lib.rs +++ b/types/src/lib.rs @@ -126,8 +126,8 @@ pub use global_state::Pointer; pub use chainspec::{ AccountConfig, AccountsConfig, ActivationPoint, AdministratorAccount, AuctionCosts, BrTableCost, Chainspec, ChainspecRawBytes, ChainspecRegistry, ConsensusProtocolName, - ControlFlowCosts, CoreConfig, DelegatorConfig, DeployConfig, FeeHandling, GenesisAccount, - GenesisConfig, GenesisValidator, GlobalStateUpdate, GlobalStateUpdateConfig, + ControlFlowCosts, CoreConfig, DelegatorConfig, DeployConfig, EvmConfig, EvmSpec, FeeHandling, + GenesisAccount, GenesisConfig, GenesisValidator, GlobalStateUpdate, GlobalStateUpdateConfig, GlobalStateUpdateError, HandlePaymentCosts, HighwayConfig, HoldBalanceHandling, HostFunction, HostFunctionCost, HostFunctionCostsV1, HostFunctionCostsV2, HostFunctionV2, LegacyRequiredFinality, MessageLimits, MintCosts, NetworkConfig, NextUpgrade, OpcodeCosts, @@ -135,7 +135,8 @@ pub use chainspec::{ StandardPaymentCosts, StorageCosts, SystemConfig, TransactionConfig, TransactionLaneDefinition, TransactionV1Config, VacancyConfig, ValidatorConfig, WasmConfig, WasmV1Config, WasmV2Config, DEFAULT_BASELINE_MOTES_AMOUNT, DEFAULT_GAS_HOLD_INTERVAL, DEFAULT_HOST_FUNCTION_NEW_DICTIONARY, - DEFAULT_MINIMUM_BID_AMOUNT, DEFAULT_REFUND_HANDLING, REWARDS_HANDLING_RATIO_TAG, + DEFAULT_MINIMUM_BID_AMOUNT, DEFAULT_REFUND_HANDLING, DEFAULT_WEI_PER_MOTE, + MINIMUM_WEI_PER_MOTE, REWARDS_HANDLING_RATIO_TAG, }; #[cfg(any(all(feature = "std", feature = "testing"), test))] pub use chainspec::{ @@ -166,8 +167,8 @@ pub use digest::{ pub use display_iter::DisplayIter; pub use era_id::EraId; pub use evm::{ - EvmAddr, EvmApproval, EvmConfig, EvmSpec, EvmTransaction, EvmTransactionError, - EvmTransactionHash, EvmTransactionKind, DEFAULT_WEI_PER_MOTE, MINIMUM_WEI_PER_MOTE, + EvmAddr, EvmApproval, EvmFeeConfig, EvmTransaction, EvmTransactionError, EvmTransactionHash, + EvmTransactionKind, }; pub use gas::Gas; #[cfg(feature = "json-schema")]