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..eff5b66777 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()))?; @@ -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()) @@ -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..edbabc2144 100644 --- a/node/src/components/transaction_buffer.rs +++ b/node/src/components/transaction_buffer.rs @@ -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, ); @@ -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..d18ed4ffa6 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( @@ -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), }) @@ -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( @@ -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), }) @@ -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), }) @@ -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..0cabb2fb68 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, @@ -83,10 +86,13 @@ impl AppendableBlock { 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 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 @@ -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, @@ -246,13 +260,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 +283,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 +304,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,6 +321,7 @@ 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(); @@ -297,7 +329,149 @@ mod tests { 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(&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.gas_limit = Gas::new(100); + let mut second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + second.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.size_estimate = 1000; + let mut second = TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + second.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.gas_limit = Gas::new(100); + let mut wasm_footprint = + TransactionFootprint::random_of_lane(LARGE_WASM_LANE_ID, &mut test_rng); + wasm_footprint.gas_limit = Gas::new(100); + let mut evm_footprint = + TransactionFootprint::random_of_lane(TEST_EVM_LANE_ID, &mut test_rng); + evm_footprint.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..f9200aac85 100644 --- a/node/src/types/block/executable_block.rs +++ b/node/src/types/block/executable_block.rs @@ -103,7 +103,8 @@ 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(); let max_block_size = chainspec.transaction_config.max_block_size as u64; let block_gas_limit = chainspec.transaction_config.block_gas_limit; @@ -116,7 +117,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..23f387f29a 100644 --- a/node/src/types/transaction/transaction_footprint.rs +++ b/node/src/types/transaction/transaction_footprint.rs @@ -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), )); 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..e24c519cbf 100644 --- a/types/src/chainspec.rs +++ b/types/src/chainspec.rs @@ -218,7 +218,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 +239,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 +249,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 +259,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 +269,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 +279,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 +427,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/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/config.rs b/types/src/evm/config.rs index 6a74531a26..4dfd4be5ad 100644 --- a/types/src/evm/config.rs +++ b/types/src/evm/config.rs @@ -5,8 +5,14 @@ use datasize::DataSize; use num_rational::Ratio; #[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 crate::chainspec::TransactionLaneDefinition; use crate::{ bytesrepr::{self, FromBytes, ToBytes, U8_SERIALIZED_LENGTH}, U256, U512, @@ -69,7 +75,7 @@ impl FromBytes for EvmSpec { } /// Chainspec configuration for EVM execution. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "datasize", derive(DataSize))] #[cfg_attr(feature = "json-schema", derive(JsonSchema))] #[serde(deny_unknown_fields)] @@ -86,6 +92,12 @@ pub struct EvmConfig { 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 { @@ -97,10 +109,58 @@ impl Default for EvmConfig { 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 EVM base fee denominated in wei. pub fn base_fee_wei(&self) -> u128 { @@ -139,6 +199,113 @@ impl EvmConfig { } } +// 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)?; @@ -147,12 +314,21 @@ impl ToBytes for EvmConfig { } fn serialized_length(&self) -> usize { - self.enabled.serialized_length() + 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() + + 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> { @@ -161,7 +337,14 @@ impl ToBytes for EvmConfig { 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) + 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(()) } } @@ -173,6 +356,12 @@ impl FromBytes for EvmConfig { 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, @@ -181,6 +370,7 @@ impl FromBytes for EvmConfig { block_gas_limit, base_fee, wei_per_mote, + transaction_lanes: transaction_lanes.map_err(|_| bytesrepr::Error::Formatting)?, }, remainder, )) @@ -234,4 +424,85 @@ mod tests { 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); + } }