From e8762225221f99b74fca80b30e67ae58ce1d8169 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:19:51 -0800 Subject: [PATCH 01/11] Add BLOCK_SPEND_LIMIT Extend deployment verification benchmarks --- console/network/src/lib.rs | 2 + ledger/benches/transaction.rs | 58 +++++++++-- ledger/src/tests.rs | 167 +++++++++++++++++++++++++++++++- synthesizer/process/src/cost.rs | 13 ++- synthesizer/process/src/lib.rs | 15 +++ synthesizer/src/vm/finalize.rs | 84 ++++++++++++++-- 6 files changed, 317 insertions(+), 22 deletions(-) diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 099044f171..b0694d43a0 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -133,6 +133,8 @@ pub trait Network: const MAX_FEE: u64 = 1_000_000_000_000_000; /// The maximum number of microcredits that can be spent on a finalize block. const TRANSACTION_SPEND_LIMIT: u64 = 100_000_000; + /// The maximum number of microcredits that can be spent finalizing executions and synthesizing deployments in a block. + const BLOCK_SPEND_LIMIT: u64 = 800_000_000; /// The anchor height, defined as the expected number of blocks to reach the coinbase target. const ANCHOR_HEIGHT: u32 = Self::ANCHOR_TIME as u32 / Self::BLOCK_TIME as u32; diff --git a/ledger/benches/transaction.rs b/ledger/benches/transaction.rs index 933b0918c0..b92be907f0 100644 --- a/ledger/benches/transaction.rs +++ b/ledger/benches/transaction.rs @@ -62,26 +62,66 @@ fn deploy(c: &mut Criterion) { // Initialize the VM. let (vm, records) = initialize_vm(&private_key, rng); - // Create a sample program. - let program = Program::::from_str( - r" -program helloworld.aleo; - -function hello: + let func = |index: usize| { + format!( + r" +function hello{index}: input r0 as u32.private; input r1 as u32.private; add r0 r1 into r2; - output r2 as u32.private; -", - ) + output r2 as u32.private;" + ) + }; + + let func_block = (0..1).map(func).reduce(|acc, e| acc + &e).unwrap(); + + // Create a sample program. + let program = Program::::from_str(&format!( + r" +program helloworld_small.aleo; + +{func_block}" + )) + .unwrap(); + + c.bench_function("Transaction::Deploy", |b| { + b.iter(|| vm.deploy(&private_key, &program, Some(records[0].clone()), 600000, None, rng).unwrap()) + }); + + // NOTE: the partially_verified_transactions LruCache causes significant speedup. + c.bench_function("Transaction::Deploy - verify", |b| { + let transaction = vm.deploy(&private_key, &program, Some(records[0].clone()), 600000, None, rng).unwrap(); + // Print num_constraints and num_variables. + if let snarkvm_ledger::Transaction::Deploy(_, _, deployment, _) = &transaction { + println!("num_combined_constraints: {}", deployment.num_combined_constraints().unwrap()); + println!("num_combined_variables: {}", deployment.num_combined_variables().unwrap()); + } + b.iter(|| vm.check_transaction(&transaction, None, rng).unwrap()) + }); + + let func_block = (0..10).map(func).reduce(|acc, e| acc + &e).unwrap(); + + // Create a bigger sample program. + let program = Program::::from_str(&format!( + r" +program helloworld_big.aleo; + +{func_block}" + )) .unwrap(); c.bench_function("Transaction::Deploy", |b| { b.iter(|| vm.deploy(&private_key, &program, Some(records[0].clone()), 600000, None, rng).unwrap()) }); + // NOTE: the partially_verified_transactions LruCache causes significant speedup. c.bench_function("Transaction::Deploy - verify", |b| { let transaction = vm.deploy(&private_key, &program, Some(records[0].clone()), 600000, None, rng).unwrap(); + // Print num_constraints and num_variables. + if let snarkvm_ledger::Transaction::Deploy(_, _, deployment, _) = &transaction { + println!("num_combined_constraints: {}", deployment.num_combined_constraints().unwrap()); + println!("num_combined_variables: {}", deployment.num_combined_variables().unwrap()); + } b.iter(|| vm.check_transaction(&transaction, None, rng).unwrap()) }); } diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index cc5989c0e5..d732d314fc 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -32,7 +32,7 @@ use ledger_committee::{Committee, MIN_VALIDATOR_STAKE}; use ledger_narwhal::{BatchCertificate, BatchHeader, Data, Subdag, Transmission, TransmissionID}; use ledger_store::{ConsensusStore, helpers::memory::ConsensusMemory}; use snarkvm_utilities::try_vm_runtime; -use synthesizer::{Stack, program::Program, vm::VM}; +use synthesizer::{Stack, prelude::cost_in_microcredits, process::synthesis_cost, program::Program, vm::VM}; use indexmap::{IndexMap, IndexSet}; use rand::seq::SliceRandom; @@ -3142,3 +3142,168 @@ fn test_forged_block_subdags() { assert!(ledger.check_next_block(&forged_block_2_from_both_subdags, &mut rand::thread_rng()).is_err()); } } + +#[test] +fn test_transactions_exceed_block_spend_limit() { + let rng = &mut TestRng::default(); + + // Initialize the test environment. + let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); + + // Construct a program that is just under the transaction spend limit and determine its finalize cost. + let mut allowed_program = None; + let mut allowed_finalize_cost = None; + for i in 0..::MAX_COMMANDS.ilog2() { + // Construct the finalize body. + let finalize_body = + (0..2.pow(i)).map(|i| format!("hash.bhp256 0field into r{i} as field;")).collect::>().join("\n"); + + // Construct the program. + let program = Program::from_str(&format!( + r"program test_max_spend_limit_{i}.aleo; + function foo: + async foo into r0; + output r0 as test_max_spend_limit_{i}.aleo/foo.future; + + finalize foo:{finalize_body}", + )) + .unwrap(); + + // Initialize a stack for the program. + let stack = Stack::::new(&ledger.vm().process().read(), &program).unwrap(); + + // Check the finalize cost. + let finalize_cost = cost_in_microcredits(&stack, &Identifier::from_str("foo").unwrap()).unwrap(); + + // If the finalize cost exceeds the maximum transaction spend, assign the program to the exceeding program and break. + // Otherwise, assign the program to the allowed program and continue. + if finalize_cost > ::TRANSACTION_SPEND_LIMIT { + break; + } else { + allowed_program = Some(program); + allowed_finalize_cost = Some(finalize_cost); + } + } + + // Ensure that the program and finalize cost are not None. + assert!(allowed_program.is_some()); + assert!(allowed_finalize_cost.is_some()); + + let program = allowed_program.unwrap(); + let finalize_cost = allowed_finalize_cost.unwrap(); + + // Deploy the program. + let deployment = ledger.vm().deploy(&private_key, &program, None, 0, None, rng).unwrap(); + + // Construct the next block. + let block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![deployment], rng).unwrap(); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + + // Add the block to the ledger. + ledger.advance_to_next_block(&block).unwrap(); + + // Generate executions whose aggregate cost exceeds the block spend limit. + let mut transactions = Vec::new(); + for _ in 0..(::BLOCK_SPEND_LIMIT / finalize_cost + 1) { + transactions.push( + ledger + .vm() + .execute( + &private_key, + (program.id(), "foo"), + Vec::>::new().iter(), + None, + 0, + None, + rng, + ) + .unwrap(), + ); + } + + // Get the number of transactions. + let num_transactions = transactions.len(); + + // Construct the next block. + let block = ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); + + // Check that all but one transaction is accepted. + assert_eq!(block.transactions().num_accepted(), num_transactions - 1); + assert_eq!(block.aborted_transaction_ids().len(), 1); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + + // Add the block. + ledger.advance_to_next_block(&block).unwrap(); +} + +#[test] +fn test_exceed_block_constraint_limit() { + let rng = &mut TestRng::default(); + + // Initialize the test environment. + let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); + + // Construct a program with 7 SHA3 hashes, which is just under the deployment spend limit. + let program = Program::from_str( + r"program test_max_deployment_limit_0.aleo; + function foo: + input r0 as [field; 20u32].private; + hash.sha3_256 r0 into r1 as field;", + ) + .unwrap(); + + // Deploy the program. + let deployment = ledger.vm().deploy(&private_key, &program, None, 0, None, rng).unwrap(); + + // Get the synthesis cost. + let synthesis_cost = synthesis_cost(deployment.deployment().unwrap()).unwrap(); + + println!("synthesis_cost: {synthesis_cost}"); + + // Construct the next block. + let block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![deployment], rng).unwrap(); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + + // Add the block to the ledger. + ledger.advance_to_next_block(&block).unwrap(); + + // Construct enough deployment transactions to exceed the block constraint limit. + let mut transactions = Vec::new(); + for i in 0..(::BLOCK_SPEND_LIMIT / synthesis_cost + 1) { + let program = Program::from_str(&format!( + r"program test_max_deployment_limit_{}.aleo; + function foo: + input r0 as [field; 20u32].private; + hash.sha3_256 r0 into r1 as field;", + i + 1 + )) + .unwrap(); + + let deployment = ledger.vm().deploy(&private_key, &program, None, 0, None, rng).unwrap(); + transactions.push(deployment) + } + + // Get the number of transactions. + let num_transactions = transactions.len(); + + // Construct the next block. + let block = ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); + + // Check that all but one transaction is accepted. + assert_eq!(block.transactions().num_accepted(), num_transactions - 1); + assert_eq!(block.aborted_transaction_ids().len(), 1); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + + // Add the block to the ledger. + ledger.advance_to_next_block(&block).unwrap(); +} diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 9be8228648..e19dddd9b3 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -30,10 +30,6 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( let program_id = deployment.program_id(); // Determine the number of characters in the program ID. let num_characters = u32::try_from(program_id.name().to_string().len())?; - // Compute the number of combined variables in the program. - let num_combined_variables = deployment.num_combined_variables()?; - // Compute the number of combined constraints in the program. - let num_combined_constraints = deployment.num_combined_constraints()?; // Compute the storage cost in microcredits. let storage_cost = size_in_bytes @@ -41,7 +37,7 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; // Compute the synthesis cost in microcredits. - let synthesis_cost = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER; + let synthesis_cost = synthesis_cost(deployment)?; // Compute the namespace cost in credits: 10^(10 - num_characters). let namespace_cost = 10u64 @@ -58,6 +54,13 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) } +pub fn synthesis_cost(deployment: &Deployment) -> Result { + // Compute the synthesis cost in microcredits. + let num_combined_variables = deployment.num_combined_variables()?; + let num_combined_constraints = deployment.num_combined_constraints()?; + Ok(num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER) +} + /// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). pub fn execution_cost(process: &Process, execution: &Execution) -> Result<(u64, (u64, u64))> { // Compute the storage cost in microcredits. diff --git a/synthesizer/process/src/lib.rs b/synthesizer/process/src/lib.rs index 65bfede98e..1dba0aaa2a 100644 --- a/synthesizer/process/src/lib.rs +++ b/synthesizer/process/src/lib.rs @@ -228,6 +228,21 @@ impl Process { Ok(self.get_stack(program_id)?.program()) } + /// Returns the finalize cost for the given program ID and function name. + #[inline] + pub fn get_finalize_cost( + &self, + program_id: impl TryInto>, + function_name: impl TryInto>, + ) -> Result { + // Prepare the program ID. + let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?; + // Prepare the function name. + let function_name = function_name.try_into().map_err(|_| anyhow!("Invalid function name"))?; + // Return the finalize cost. + self.get_stack(program_id)?.get_finalize_cost(&function_name) + } + /// Returns the proving key for the given program ID and function name. #[inline] pub fn get_proving_key( diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 2cbef6359a..9cf34bdd24 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -16,6 +16,7 @@ use super::*; use ledger_committee::{MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE}; +use synthesizer_process::synthesis_cost; use utilities::cfg_sort_by_cached_key; impl> VM { @@ -178,7 +179,7 @@ impl> VM { /// The maximum number of confirmed transactions allowed in a block. /// This is deliberately set to a low value (8) for testing purposes only. #[cfg(any(test, feature = "test"))] - pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = 8; + pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = 256; /// Performs atomic speculation over a list of transactions. /// @@ -286,6 +287,8 @@ impl> VM { let mut tpks: IndexSet> = IndexSet::new(); // Initialize the list of deployment payers. let mut deployment_payers: IndexSet> = IndexSet::new(); + // Initialize a counter for the total amount of microcredits spent on compute. + let mut microcredits_spent_on_compute = 0u64; // Finalize the transactions. 'outer: for transaction in transactions { @@ -300,12 +303,14 @@ impl> VM { // Determine if the transaction should be aborted. if let Some(reason) = self.should_abort_transaction( + &process, transaction, &transition_ids, &input_ids, &output_ids, &tpks, &deployment_payers, + microcredits_spent_on_compute, ) { // Store the aborted transaction. aborted.push((transaction.clone(), reason)); @@ -437,9 +442,25 @@ impl> VM { output_ids.extend(confirmed_transaction.transaction().output_ids()); // Add the transition public keys to the set of produced transition public keys. tpks.extend(confirmed_transaction.transaction().transition_public_keys()); - // Add any public deployment payer to the set of deployment payers. - if let Transaction::Deploy(_, _, _, fee) = confirmed_transaction.transaction() { + if let Transaction::Deploy(_, _, deployment, fee) = confirmed_transaction.transaction() { + // Add any public deployment payer to the set of deployment payers. fee.payer().map(|payer| deployment_payers.insert(payer)); + // Compute the synthesis cost. + let synthesis_cost = synthesis_cost(deployment).map_err(|e| e.to_string())?; + // Add the synthesis cost to the total microcredits spent on compute. + microcredits_spent_on_compute = + microcredits_spent_on_compute.saturating_add(synthesis_cost); + } + // Add any finalize cost to the total finalize cost. + if let Transaction::Execute(_, execution, _) = confirmed_transaction.transaction() { + // Get the root transition from the execution. + let root_transition = execution.peek().map_err(|e| e.to_string())?; + // Get the finalize cost from the process. + let finalize_cost = process + .get_finalize_cost(root_transition.program_id(), root_transition.function_name()) + .map_err(|e| e.to_string())?; + // Add the finalize cost to the total microcredits spent on compute. + microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(finalize_cost); } // Store the confirmed transaction. confirmed.push(confirmed_transaction); @@ -768,14 +789,18 @@ impl> VM { /// - The transaction is producing a duplicate output /// - The transaction is producing a duplicate transition public key /// - The transaction is another deployment in the block from the same public fee payer. + /// - The transaction is an execution that exceeds the block spend limit. + #[allow(clippy::too_many_arguments)] fn should_abort_transaction( &self, + process: &Process, transaction: &Transaction, transition_ids: &IndexSet, input_ids: &IndexSet>, output_ids: &IndexSet>, tpks: &IndexSet>, deployment_payers: &IndexSet>, + microcredits_spent_on_compute: u64, ) -> Option { // Ensure that the transaction is not producing a duplicate transition. for transition_id in transaction.transition_ids() { @@ -813,13 +838,37 @@ impl> VM { } // If the transaction is a deployment, ensure that it is not another deployment in the block from the same public fee payer. - if let Transaction::Deploy(_, _, _, fee) = transaction { + if let Transaction::Deploy(_, _, deployment, fee) = transaction { // If any public deployment payer has already deployed in this block, abort the transaction. if let Some(payer) = fee.payer() { if deployment_payers.contains(&payer) { return Some(format!("Another deployment in the block from the same public fee payer {payer}")); } } + // Check that the synthesis cost does not exceed the maximum. + let Ok(synthesis_cost) = synthesis_cost(deployment) else { + return Some("Failed to get synthesis cost".to_string()); + }; + // If the synthesis cost exceeds the block spend limit, abort the transaction. + if synthesis_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { + return Some("Exceeds block spend limit".to_string()); + } + } + + // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. + if let Transaction::Execute(_, execution, _) = transaction { + // Get the root transition from the execution. + let Ok(root) = execution.peek() else { + return Some("Failed to get root transition".to_string()); + }; + // Get the finalize cost from the process. + let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { + return Some("Failed to get finalize cost".to_string()); + }; + // If the finalize cost exceeds the block spend limit, abort the transaction. + if finalize_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { + return Some("Exceeds block spend limit".to_string()); + } } // Return `None` because the transaction is well-formed. @@ -852,6 +901,8 @@ impl> VM { let mut tpks: IndexSet> = Default::default(); // Initialize the list of deployment payers. let mut deployment_payers: IndexSet> = Default::default(); + // Initialize a counter for the total amount of microcredits spent on compute. + let mut microcredits_spent_on_compute = 0u64; // Abort the transactions that are have duplicates or are invalid. This will prevent the VM from performing // verification on transactions that would have been aborted in `VM::atomic_speculate`. @@ -864,12 +915,14 @@ impl> VM { // Determine if the transaction should be aborted. match self.should_abort_transaction( + &self.process.read(), transaction, &transition_ids, &input_ids, &output_ids, &tpks, &deployment_payers, + microcredits_spent_on_compute, ) { // Store the aborted transaction. Some(reason) => aborted_transactions.push((*transaction, reason.to_string())), @@ -883,9 +936,26 @@ impl> VM { output_ids.extend(transaction.output_ids()); // Add the transition public keys to the set of produced transition public keys. tpks.extend(transaction.transition_public_keys()); - // Add any public deployment payer to the set of deployment payers. - if let Transaction::Deploy(_, _, _, fee) = transaction { + + if let Transaction::Deploy(_, _, deployment, fee) = transaction { + // Add any public deployment payer to the set of deployment payers. fee.payer().map(|payer| deployment_payers.insert(payer)); + // Compute the synthesis cost. + let synthesis_cost = synthesis_cost(deployment)?; + // Add the synthesis cost to the total microcredits spent on compute. + microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(synthesis_cost); + } + // Add any finalize cost to the total finalize cost. + if let Transaction::Execute(_, execution, _) = transaction { + // Get the root transition from the execution. + let root_transition = execution.peek()?; + // Get the finalize cost from the process. + let finalize_cost = self + .process + .read() + .get_finalize_cost(root_transition.program_id(), root_transition.function_name())?; + // Add the finalize cost to the total microcredits spent on compute. + microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(finalize_cost); } // Add the transaction to the list of transactions to verify. @@ -1569,7 +1639,7 @@ finalize transfer_public: // Prepare the additional fee. let view_key = ViewKey::::try_from(caller_private_key).unwrap(); - let credits = Some(unspent_records.pop().unwrap().decrypt(&view_key).unwrap()); + let credits = unspent_records.pop().and_then(|record| record.decrypt(&view_key).ok()); // Execute. let transaction = vm From 2bae0f2dbfa87bd4ab1f9ffef4a2bc055ea0c583 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 12:45:41 +0200 Subject: [PATCH 02/11] Ensure block spend limit only applies from CONSENSUS_V2_HEIGHT --- console/network/src/canary_v0.rs | 3 +++ console/network/src/lib.rs | 2 ++ console/network/src/mainnet_v0.rs | 3 +++ console/network/src/testnet_v0.rs | 3 +++ synthesizer/src/vm/finalize.rs | 37 +++++++++++++++++++------------ 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/console/network/src/canary_v0.rs b/console/network/src/canary_v0.rs index f0a973acd9..6f9a230da9 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -133,6 +133,9 @@ impl Network for CanaryV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which new consensus rules apply. + // TODO: adjust based on canary height. + const CONSENSUS_V2_HEIGHT: u32 = 1_000; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index b0694d43a0..483157e15c 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -203,6 +203,8 @@ pub trait Network: /// The maximum number of certificates in a batch. const MAX_CERTIFICATES: u16; + /// The block height from which new consensus rules apply. + const CONSENSUS_V2_HEIGHT: u32; /// The maximum number of bytes in a transaction. // Note: This value must **not** be decreased as it would invalidate existing transactions. diff --git a/console/network/src/mainnet_v0.rs b/console/network/src/mainnet_v0.rs index 6aadaa3f38..540b5eb878 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -134,6 +134,9 @@ impl Network for MainnetV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which new consensus rules apply. + // TODO: adjust based on mainnet height. + const CONSENSUS_V2_HEIGHT: u32 = 3_000_000; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/testnet_v0.rs b/console/network/src/testnet_v0.rs index fd793055a4..e6abda84d9 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -133,6 +133,9 @@ impl Network for TestnetV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which new consensus rules apply. + // TODO: adjust based on testnet height. + const CONSENSUS_V2_HEIGHT: u32 = 1_000; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 9cf34bdd24..b28b2713eb 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -802,6 +802,9 @@ impl> VM { deployment_payers: &IndexSet>, microcredits_spent_on_compute: u64, ) -> Option { + // Get the current block height to help determine which checks to perform. + let current_block_height = self.block_store().current_block_height(); + // Ensure that the transaction is not producing a duplicate transition. for transition_id in transaction.transition_ids() { // If the transition ID is already produced in this block or previous blocks, abort the transaction. @@ -845,13 +848,16 @@ impl> VM { return Some(format!("Another deployment in the block from the same public fee payer {payer}")); } } - // Check that the synthesis cost does not exceed the maximum. - let Ok(synthesis_cost) = synthesis_cost(deployment) else { - return Some("Failed to get synthesis cost".to_string()); - }; - // If the synthesis cost exceeds the block spend limit, abort the transaction. - if synthesis_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { - return Some("Exceeds block spend limit".to_string()); + // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. + if current_block_height >= N::CONSENSUS_V2_HEIGHT { + // Check that the synthesis cost does not exceed the maximum. + let Ok(synthesis_cost) = synthesis_cost(deployment) else { + return Some("Failed to get synthesis cost".to_string()); + }; + // If the synthesis cost exceeds the block spend limit, abort the transaction. + if synthesis_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { + return Some("Exceeds block spend limit".to_string()); + } } } @@ -861,13 +867,16 @@ impl> VM { let Ok(root) = execution.peek() else { return Some("Failed to get root transition".to_string()); }; - // Get the finalize cost from the process. - let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { - return Some("Failed to get finalize cost".to_string()); - }; - // If the finalize cost exceeds the block spend limit, abort the transaction. - if finalize_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { - return Some("Exceeds block spend limit".to_string()); + // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. + if current_block_height >= N::CONSENSUS_V2_HEIGHT { + // Get the finalize cost from the process. + let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { + return Some("Failed to get finalize cost".to_string()); + }; + // If the finalize cost exceeds the block spend limit, abort the transaction. + if finalize_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { + return Some("Exceeds block spend limit".to_string()); + } } } From 86af81a437144ee1092a6c3acd94bdd217101357 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 14:43:07 +0200 Subject: [PATCH 03/11] Introduce EXECUTION_FIXED_COST --- console/network/src/lib.rs | 5 ++++- ledger/src/tests.rs | 2 +- synthesizer/process/src/cost.rs | 8 +++++++- synthesizer/src/vm/finalize.rs | 21 ++++++++++++++------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 483157e15c..9d72eebc21 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -133,8 +133,11 @@ pub trait Network: const MAX_FEE: u64 = 1_000_000_000_000_000; /// The maximum number of microcredits that can be spent on a finalize block. const TRANSACTION_SPEND_LIMIT: u64 = 100_000_000; + /// The fixed cost in microcredits to verify an execution. + // NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. + const EXECUTION_FIXED_COST: u64 = 2_000_000; // 2 million microcredits /// The maximum number of microcredits that can be spent finalizing executions and synthesizing deployments in a block. - const BLOCK_SPEND_LIMIT: u64 = 800_000_000; + const BLOCK_SPEND_LIMIT: u64 = 950_000_000; /// The anchor height, defined as the expected number of blocks to reach the coinbase target. const ANCHOR_HEIGHT: u32 = Self::ANCHOR_TIME as u32 / Self::BLOCK_TIME as u32; diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index d732d314fc..cbaaa9a68a 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -3242,7 +3242,7 @@ fn test_transactions_exceed_block_spend_limit() { } #[test] -fn test_exceed_block_constraint_limit() { +fn test_exceed_block_spend_limit_deployments() { let rng = &mut TestRng::default(); // Initialize the test environment. diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index e19dddd9b3..27016acafb 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -54,8 +54,8 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) } +/// Returns the cost in microcredits to synthesize a deployment. pub fn synthesis_cost(deployment: &Deployment) -> Result { - // Compute the synthesis cost in microcredits. let num_combined_variables = deployment.num_combined_variables()?; let num_combined_constraints = deployment.num_combined_constraints()?; Ok(num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER) @@ -89,6 +89,12 @@ fn execution_storage_cost(size_in_bytes: u64) -> u64 { } } +/// Returns the fixed cost for an execution. +/// NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. +pub fn execution_fixed_cost() -> u64 { + N::EXECUTION_FIXED_COST +} + /// Finalize costs for compute heavy operations, derived as: /// `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`. diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index b28b2713eb..f003440057 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -16,7 +16,7 @@ use super::*; use ledger_committee::{MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE}; -use synthesizer_process::synthesis_cost; +use synthesizer_process::{execution_fixed_cost, synthesis_cost}; use utilities::cfg_sort_by_cached_key; impl> VM { @@ -459,8 +459,11 @@ impl> VM { let finalize_cost = process .get_finalize_cost(root_transition.program_id(), root_transition.function_name()) .map_err(|e| e.to_string())?; - // Add the finalize cost to the total microcredits spent on compute. - microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(finalize_cost); + // Add the fixed execution cost. + let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); + // Add the execution cost to the total microcredits spent on compute. + microcredits_spent_on_compute = + microcredits_spent_on_compute.saturating_add(execution_cost); } // Store the confirmed transaction. confirmed.push(confirmed_transaction); @@ -873,8 +876,10 @@ impl> VM { let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { return Some("Failed to get finalize cost".to_string()); }; - // If the finalize cost exceeds the block spend limit, abort the transaction. - if finalize_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { + // Add the fixed execution cost. + let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); + // If the execution cost exceeds the block spend limit, abort the transaction. + if execution_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { return Some("Exceeds block spend limit".to_string()); } } @@ -963,8 +968,10 @@ impl> VM { .process .read() .get_finalize_cost(root_transition.program_id(), root_transition.function_name())?; - // Add the finalize cost to the total microcredits spent on compute. - microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(finalize_cost); + // Add the fixed execution cost. + let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); + // Add the execution cost to the total microcredits spent on compute. + microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(execution_cost); } // Add the transaction to the list of transactions to verify. From 3850bc19dcb4acaf24919006386149c0994ae0a9 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 14:51:20 +0200 Subject: [PATCH 04/11] Comment and ordering nits --- console/network/src/lib.rs | 2 +- synthesizer/src/vm/finalize.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 9d72eebc21..8c383d3c6d 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -136,7 +136,7 @@ pub trait Network: /// The fixed cost in microcredits to verify an execution. // NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. const EXECUTION_FIXED_COST: u64 = 2_000_000; // 2 million microcredits - /// The maximum number of microcredits that can be spent finalizing executions and synthesizing deployments in a block. + /// The maximum number of microcredits that can be spent in a block. const BLOCK_SPEND_LIMIT: u64 = 950_000_000; /// The anchor height, defined as the expected number of blocks to reach the coinbase target. diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index f003440057..afd1dfc99d 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -792,7 +792,7 @@ impl> VM { /// - The transaction is producing a duplicate output /// - The transaction is producing a duplicate transition public key /// - The transaction is another deployment in the block from the same public fee payer. - /// - The transaction is an execution that exceeds the block spend limit. + /// - The transaction exceeds the block spend limit. #[allow(clippy::too_many_arguments)] fn should_abort_transaction( &self, @@ -864,14 +864,14 @@ impl> VM { } } - // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. - if let Transaction::Execute(_, execution, _) = transaction { - // Get the root transition from the execution. - let Ok(root) = execution.peek() else { - return Some("Failed to get root transition".to_string()); - }; - // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. - if current_block_height >= N::CONSENSUS_V2_HEIGHT { + // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. + if current_block_height >= N::CONSENSUS_V2_HEIGHT { + // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. + if let Transaction::Execute(_, execution, _) = transaction { + // Get the root transition from the execution. + let Ok(root) = execution.peek() else { + return Some("Failed to get root transition".to_string()); + }; // Get the finalize cost from the process. let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { return Some("Failed to get finalize cost".to_string()); From b92d9f1b501ef702c133f0f5f894351ee27e537e Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 18:35:36 +0200 Subject: [PATCH 05/11] Prevent overflow --- ledger/store/src/block/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/store/src/block/mod.rs b/ledger/store/src/block/mod.rs index d507ca0ef5..e04db4bc98 100644 --- a/ledger/store/src/block/mod.rs +++ b/ledger/store/src/block/mod.rs @@ -1197,7 +1197,7 @@ impl> BlockStore { /// Returns the current block height. pub fn current_block_height(&self) -> u32 { - u32::try_from(self.tree.read().number_of_leaves()).unwrap() - 1 + u32::try_from(self.tree.read().number_of_leaves()).unwrap().saturating_sub(1) } /// Returns the state root that contains the given `block height`. From 3443c3ef79c26696f83e27f7a10a6599ec3310d7 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 18:35:45 +0200 Subject: [PATCH 06/11] Make const fn const --- synthesizer/process/src/cost.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 27016acafb..73a09d72a7 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -91,7 +91,7 @@ fn execution_storage_cost(size_in_bytes: u64) -> u64 { /// Returns the fixed cost for an execution. /// NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. -pub fn execution_fixed_cost() -> u64 { +pub const fn execution_fixed_cost() -> u64 { N::EXECUTION_FIXED_COST } From fcbbb688b852e221060921511a449dcac5613c7f Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Fri, 25 Oct 2024 18:39:06 +0200 Subject: [PATCH 07/11] Clean up CONSENSUS_V2_HEIGHT logic --- synthesizer/src/vm/finalize.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index afd1dfc99d..b1a282577c 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -851,7 +851,7 @@ impl> VM { return Some(format!("Another deployment in the block from the same public fee payer {payer}")); } } - // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. + // Activate deployment checks from CONSENSUS_V2_HEIGHT onwards. if current_block_height >= N::CONSENSUS_V2_HEIGHT { // Check that the synthesis cost does not exceed the maximum. let Ok(synthesis_cost) = synthesis_cost(deployment) else { @@ -864,10 +864,10 @@ impl> VM { } } - // Activate extra checks from CONSENSUS_V2_HEIGHT onwards. - if current_block_height >= N::CONSENSUS_V2_HEIGHT { - // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. - if let Transaction::Execute(_, execution, _) = transaction { + // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. + if let Transaction::Execute(_, execution, _) = transaction { + // Activate execute checks from CONSENSUS_V2_HEIGHT onwards. + if current_block_height >= N::CONSENSUS_V2_HEIGHT { // Get the root transition from the execution. let Ok(root) = execution.peek() else { return Some("Failed to get root transition".to_string()); From 3fd635436c7d717149d7dfba32ba81038b559a9d Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Sun, 27 Oct 2024 17:04:56 +0100 Subject: [PATCH 08/11] Fix tests --- .circleci/config.yml | 1 + console/network/src/canary_v0.rs | 4 ++ console/network/src/mainnet_v0.rs | 4 ++ console/network/src/testnet_v0.rs | 4 ++ ledger/src/tests.rs | 79 +++++++++++++++++++++++-------- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4aad608529..236653f780 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -497,6 +497,7 @@ jobs: resource_class: << pipeline.parameters.xlarge >> steps: - run_serial: + flags: --features=test workspace_member: ledger cache_key: v1.0.0-rust-1.81.0-snarkvm-ledger-cache diff --git a/console/network/src/canary_v0.rs b/console/network/src/canary_v0.rs index 6f9a230da9..990dab48d1 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -135,7 +135,11 @@ impl Network for CanaryV0 { /// The block height from which new consensus rules apply. // TODO: adjust based on canary height. + #[cfg(not(any(test, feature = "test")))] const CONSENSUS_V2_HEIGHT: u32 = 1_000; + /// The block height from which new consensus rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 0; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/mainnet_v0.rs b/console/network/src/mainnet_v0.rs index 540b5eb878..0142cf9045 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -136,7 +136,11 @@ impl Network for MainnetV0 { /// The block height from which new consensus rules apply. // TODO: adjust based on mainnet height. + #[cfg(not(any(test, feature = "test")))] const CONSENSUS_V2_HEIGHT: u32 = 3_000_000; + /// The block height from which new consensus rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 0; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/testnet_v0.rs b/console/network/src/testnet_v0.rs index e6abda84d9..e841798d49 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -135,7 +135,11 @@ impl Network for TestnetV0 { /// The block height from which new consensus rules apply. // TODO: adjust based on testnet height. + #[cfg(not(any(test, feature = "test")))] const CONSENSUS_V2_HEIGHT: u32 = 1_000; + /// The block height from which new consensus rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 0; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index cbaaa9a68a..f10c950f01 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -32,7 +32,12 @@ use ledger_committee::{Committee, MIN_VALIDATOR_STAKE}; use ledger_narwhal::{BatchCertificate, BatchHeader, Data, Subdag, Transmission, TransmissionID}; use ledger_store::{ConsensusStore, helpers::memory::ConsensusMemory}; use snarkvm_utilities::try_vm_runtime; -use synthesizer::{Stack, prelude::cost_in_microcredits, process::synthesis_cost, program::Program, vm::VM}; +use synthesizer::{ + Stack, + process::synthesis_cost, + program::{Program, StackProgram}, + vm::VM, +}; use indexmap::{IndexMap, IndexSet}; use rand::seq::SliceRandom; @@ -3144,7 +3149,7 @@ fn test_forged_block_subdags() { } #[test] -fn test_transactions_exceed_block_spend_limit() { +fn test_executions_exceed_block_spend_limit() { let rng = &mut TestRng::default(); // Initialize the test environment. @@ -3170,18 +3175,15 @@ fn test_transactions_exceed_block_spend_limit() { .unwrap(); // Initialize a stack for the program. - let stack = Stack::::new(&ledger.vm().process().read(), &program).unwrap(); - - // Check the finalize cost. - let finalize_cost = cost_in_microcredits(&stack, &Identifier::from_str("foo").unwrap()).unwrap(); - - // If the finalize cost exceeds the maximum transaction spend, assign the program to the exceeding program and break. - // Otherwise, assign the program to the allowed program and continue. - if finalize_cost > ::TRANSACTION_SPEND_LIMIT { - break; - } else { + // If we succeed, the finalize cost must be below the TRANSACTION_SPEND_LIMIT. + if let Ok(stack) = Stack::::new(&ledger.vm().process().read(), &program) { + // Get the finalize cost from the stack. + let finalize_cost = stack.get_finalize_cost(&Identifier::from_str("foo").unwrap()).unwrap(); + // Set the program and finalize cost. allowed_program = Some(program); allowed_finalize_cost = Some(finalize_cost); + } else { + break; } } @@ -3242,7 +3244,7 @@ fn test_transactions_exceed_block_spend_limit() { } #[test] -fn test_exceed_block_spend_limit_deployments() { +fn test_deployments_exceed_block_spend_limit() { let rng = &mut TestRng::default(); // Initialize the test environment. @@ -3263,7 +3265,8 @@ fn test_exceed_block_spend_limit_deployments() { // Get the synthesis cost. let synthesis_cost = synthesis_cost(deployment.deployment().unwrap()).unwrap(); - println!("synthesis_cost: {synthesis_cost}"); + // Determine number of deployments that cannot be included in a block. + let num_deployments = usize::try_from(::BLOCK_SPEND_LIMIT / synthesis_cost + 1).unwrap(); // Construct the next block. let block = @@ -3275,8 +3278,41 @@ fn test_exceed_block_spend_limit_deployments() { // Add the block to the ledger. ledger.advance_to_next_block(&block).unwrap(); - // Construct enough deployment transactions to exceed the block constraint limit. - let mut transactions = Vec::new(); + // Prepare unique addresses to fund. + // Deployments need to come from unique identities in order to be accepted in a block. + let mut new_private_keys = Vec::with_capacity(num_deployments); + let mut new_addresses = Vec::with_capacity(num_deployments); + let mut funding_transactions = Vec::with_capacity(num_deployments); + + // Generate funding transactions. + for _ in 0..num_deployments { + // Sample recipients. + let recipient_private_key = PrivateKey::::new(rng).unwrap(); + new_private_keys.push(recipient_private_key); + let recipient_address = Address::try_from(&recipient_private_key).unwrap(); + new_addresses.push(recipient_address); + + // Fund the recipient with 1 million credits. + let inputs = + [Value::from_str(&format!("{recipient_address}")).unwrap(), Value::from_str("1000000000000u64").unwrap()]; + let transaction = ledger + .vm + .execute(&private_key, ("credits.aleo", "transfer_public"), inputs.into_iter(), None, 0, None, rng) + .unwrap(); + + funding_transactions.push(transaction); + } + // Generate block funding recipients. + let block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], funding_transactions, rng).unwrap(); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + // Add the deployment block to the ledger. + ledger.advance_to_next_block(&block).unwrap(); + + // Construct enough deployment transactions to exceed the block spend limit. + let mut deployment_transactions = Vec::with_capacity(num_deployments); for i in 0..(::BLOCK_SPEND_LIMIT / synthesis_cost + 1) { let program = Program::from_str(&format!( r"program test_max_deployment_limit_{}.aleo; @@ -3287,15 +3323,18 @@ fn test_exceed_block_spend_limit_deployments() { )) .unwrap(); - let deployment = ledger.vm().deploy(&private_key, &program, None, 0, None, rng).unwrap(); - transactions.push(deployment) + let deployment = + ledger.vm().deploy(&new_private_keys[usize::try_from(i).unwrap()], &program, None, 0, None, rng).unwrap(); + deployment_transactions.push(deployment) } // Get the number of transactions. - let num_transactions = transactions.len(); + let num_transactions = deployment_transactions.len(); // Construct the next block. - let block = ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], transactions, rng).unwrap(); + let block = ledger + .prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], deployment_transactions, rng) + .unwrap(); // Check that all but one transaction is accepted. assert_eq!(block.transactions().num_accepted(), num_transactions - 1); From e6fe7357ce4880cdb208c258d963ee85e2a71611 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Sun, 27 Oct 2024 19:07:04 +0100 Subject: [PATCH 09/11] DRY up tx checks using SeenTransactionDetails helper struct --- synthesizer/src/vm/finalize.rs | 154 ++++++++++++++++++--------------- synthesizer/src/vm/mod.rs | 6 +- 2 files changed, 85 insertions(+), 75 deletions(-) diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index b1a282577c..134074c62e 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -15,9 +15,62 @@ use super::*; -use ledger_committee::{MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE}; -use synthesizer_process::{execution_fixed_cost, synthesis_cost}; -use utilities::cfg_sort_by_cached_key; +/// Helper struct to store the details of previously seen transactions. +/// Transactions with duplicate information must be aborted. +struct SeenTransactionDetails { + transition_ids: IndexSet, + input_ids: IndexSet>, + output_ids: IndexSet>, + tpks: IndexSet>, + deployment_payers: IndexSet>, +} + +impl SeenTransactionDetails { + fn new() -> Self { + Self { + transition_ids: IndexSet::new(), + input_ids: IndexSet::new(), + output_ids: IndexSet::new(), + tpks: IndexSet::new(), + deployment_payers: IndexSet::new(), + } + } + + fn transition_ids(&self) -> &IndexSet { + &self.transition_ids + } + + fn input_ids(&self) -> &IndexSet> { + &self.input_ids + } + + fn output_ids(&self) -> &IndexSet> { + &self.output_ids + } + + fn tpks(&self) -> &IndexSet> { + &self.tpks + } + + fn deployment_payers(&self) -> &IndexSet> { + &self.deployment_payers + } + + fn insert_transaction_details(&mut self, transaction: &Transaction) { + // Add the transition IDs to the set of produced transition IDs. + self.transition_ids.extend(transaction.transition_ids()); + // Add the input IDs to the set of spent input IDs. + self.input_ids.extend(transaction.input_ids()); + // Add the output IDs to the set of produced output IDs. + self.output_ids.extend(transaction.output_ids()); + // Add the transition public keys to the set of produced transition public keys. + self.tpks.extend(transaction.transition_public_keys()); + // Add any public deployment payer to the set of deployment payers. + if let Transaction::Deploy(_, _, _, fee) = transaction { + fee.payer().map(|payer| self.deployment_payers.insert(payer)); + } + } +} impl> VM { /// Speculates on the given list of transactions in the VM. @@ -277,16 +330,8 @@ impl> VM { let mut deployments = IndexSet::new(); // Initialize a counter for the confirmed transaction index. let mut counter = 0u32; - // Initialize a list of created transition IDs. - let mut transition_ids: IndexSet = IndexSet::new(); - // Initialize a list of spent input IDs. - let mut input_ids: IndexSet> = IndexSet::new(); - // Initialize a list of created output IDs. - let mut output_ids: IndexSet> = IndexSet::new(); - // Initialize the list of created transition public keys. - let mut tpks: IndexSet> = IndexSet::new(); - // Initialize the list of deployment payers. - let mut deployment_payers: IndexSet> = IndexSet::new(); + // Initialize seen transaction details. + let mut tx_details = SeenTransactionDetails::new(); // Initialize a counter for the total amount of microcredits spent on compute. let mut microcredits_spent_on_compute = 0u64; @@ -302,16 +347,9 @@ impl> VM { } // Determine if the transaction should be aborted. - if let Some(reason) = self.should_abort_transaction( - &process, - transaction, - &transition_ids, - &input_ids, - &output_ids, - &tpks, - &deployment_payers, - microcredits_spent_on_compute, - ) { + if let Some(reason) = + self.should_abort_transaction(&process, transaction, &tx_details, microcredits_spent_on_compute) + { // Store the aborted transaction. aborted.push((transaction.clone(), reason)); // Continue to the next transaction. @@ -434,17 +472,10 @@ impl> VM { match outcome { // If the transaction succeeded, store it and continue to the next transaction. Ok(confirmed_transaction) => { - // Add the transition IDs to the set of produced transition IDs. - transition_ids.extend(confirmed_transaction.transaction().transition_ids()); - // Add the input IDs to the set of spent input IDs. - input_ids.extend(confirmed_transaction.transaction().input_ids()); - // Add the output IDs to the set of produced output IDs. - output_ids.extend(confirmed_transaction.transaction().output_ids()); - // Add the transition public keys to the set of produced transition public keys. - tpks.extend(confirmed_transaction.transaction().transition_public_keys()); - if let Transaction::Deploy(_, _, deployment, fee) = confirmed_transaction.transaction() { - // Add any public deployment payer to the set of deployment payers. - fee.payer().map(|payer| deployment_payers.insert(payer)); + // Add the transaction details to the seen transaction details. + tx_details.insert_transaction_details(confirmed_transaction.transaction()); + // Add any synthesis cost to the total microcredits spent on compute. + if let Transaction::Deploy(_, _, deployment, _) = confirmed_transaction.transaction() { // Compute the synthesis cost. let synthesis_cost = synthesis_cost(deployment).map_err(|e| e.to_string())?; // Add the synthesis cost to the total microcredits spent on compute. @@ -793,16 +824,11 @@ impl> VM { /// - The transaction is producing a duplicate transition public key /// - The transaction is another deployment in the block from the same public fee payer. /// - The transaction exceeds the block spend limit. - #[allow(clippy::too_many_arguments)] fn should_abort_transaction( &self, process: &Process, transaction: &Transaction, - transition_ids: &IndexSet, - input_ids: &IndexSet>, - output_ids: &IndexSet>, - tpks: &IndexSet>, - deployment_payers: &IndexSet>, + tx_details: &SeenTransactionDetails, microcredits_spent_on_compute: u64, ) -> Option { // Get the current block height to help determine which checks to perform. @@ -811,7 +837,7 @@ impl> VM { // Ensure that the transaction is not producing a duplicate transition. for transition_id in transaction.transition_ids() { // If the transition ID is already produced in this block or previous blocks, abort the transaction. - if transition_ids.contains(transition_id) + if tx_details.transition_ids().contains(transition_id) || self.transition_store().contains_transition_id(transition_id).unwrap_or(true) { return Some(format!("Duplicate transition {transition_id}")); @@ -821,7 +847,9 @@ impl> VM { // Ensure that the transaction is not double-spending an input. for input_id in transaction.input_ids() { // If the input ID is already spent in this block or previous blocks, abort the transaction. - if input_ids.contains(input_id) || self.transition_store().contains_input_id(input_id).unwrap_or(true) { + if tx_details.input_ids().contains(input_id) + || self.transition_store().contains_input_id(input_id).unwrap_or(true) + { return Some(format!("Double-spending input {input_id}")); } } @@ -829,7 +857,9 @@ impl> VM { // Ensure that the transaction is not producing a duplicate output. for output_id in transaction.output_ids() { // If the output ID is already produced in this block or previous blocks, abort the transaction. - if output_ids.contains(output_id) || self.transition_store().contains_output_id(output_id).unwrap_or(true) { + if tx_details.output_ids().contains(output_id) + || self.transition_store().contains_output_id(output_id).unwrap_or(true) + { return Some(format!("Duplicate output {output_id}")); } } @@ -838,7 +868,7 @@ impl> VM { // Note that the tpk and tcm are corresponding, so a uniqueness check for just the tpk is sufficient. for tpk in transaction.transition_public_keys() { // If the transition public key is already produced in this block or previous blocks, abort the transaction. - if tpks.contains(tpk) || self.transition_store().contains_tpk(tpk).unwrap_or(true) { + if tx_details.tpks().contains(tpk) || self.transition_store().contains_tpk(tpk).unwrap_or(true) { return Some(format!("Duplicate transition public key {tpk}")); } } @@ -847,7 +877,7 @@ impl> VM { if let Transaction::Deploy(_, _, deployment, fee) = transaction { // If any public deployment payer has already deployed in this block, abort the transaction. if let Some(payer) = fee.payer() { - if deployment_payers.contains(&payer) { + if tx_details.deployment_payers().contains(&payer) { return Some(format!("Another deployment in the block from the same public fee payer {payer}")); } } @@ -905,16 +935,8 @@ impl> VM { let mut valid_transactions = Vec::with_capacity(transactions.len()); let mut aborted_transactions = Vec::with_capacity(transactions.len()); - // Initialize a list of created transition IDs. - let mut transition_ids: IndexSet = Default::default(); - // Initialize a list of spent input IDs. - let mut input_ids: IndexSet> = Default::default(); - // Initialize a list of created output IDs. - let mut output_ids: IndexSet> = Default::default(); - // Initialize the list of created transition public keys. - let mut tpks: IndexSet> = Default::default(); - // Initialize the list of deployment payers. - let mut deployment_payers: IndexSet> = Default::default(); + // Initialize seen transaction details. + let mut tx_details = SeenTransactionDetails::new(); // Initialize a counter for the total amount of microcredits spent on compute. let mut microcredits_spent_on_compute = 0u64; @@ -931,29 +953,17 @@ impl> VM { match self.should_abort_transaction( &self.process.read(), transaction, - &transition_ids, - &input_ids, - &output_ids, - &tpks, - &deployment_payers, + &tx_details, microcredits_spent_on_compute, ) { // Store the aborted transaction. Some(reason) => aborted_transactions.push((*transaction, reason.to_string())), // Track the transaction state. None => { - // Add the transition IDs to the set of produced transition IDs. - transition_ids.extend(transaction.transition_ids()); - // Add the input IDs to the set of spent input IDs. - input_ids.extend(transaction.input_ids()); - // Add the output IDs to the set of produced output IDs. - output_ids.extend(transaction.output_ids()); - // Add the transition public keys to the set of produced transition public keys. - tpks.extend(transaction.transition_public_keys()); - - if let Transaction::Deploy(_, _, deployment, fee) = transaction { - // Add any public deployment payer to the set of deployment payers. - fee.payer().map(|payer| deployment_payers.insert(payer)); + // Add the transaction details to the seen transaction details. + tx_details.insert_transaction_details(*transaction); + + if let Transaction::Deploy(_, _, deployment, _) = transaction { // Compute the synthesis cost. let synthesis_cost = synthesis_cost(deployment)?; // Add the synthesis cost to the total microcredits spent on compute. diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 0be0f85440..76608d229f 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -44,7 +44,7 @@ use ledger_block::{ Transaction, Transactions, }; -use ledger_committee::Committee; +use ledger_committee::{Committee, MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE}; use ledger_narwhal_data::Data; use ledger_puzzle::Puzzle; use ledger_query::Query; @@ -59,9 +59,9 @@ use ledger_store::{ TransitionStore, atomic_finalize, }; -use synthesizer_process::{Authorization, Process, Trace, deployment_cost, execution_cost}; +use process::{Authorization, Process, Trace, deployment_cost, execution_cost, execution_fixed_cost, synthesis_cost}; use synthesizer_program::{FinalizeGlobalState, FinalizeOperation, FinalizeStoreTrait, Program}; -use utilities::try_vm_runtime; +use utilities::{cfg_sort_by_cached_key, try_vm_runtime}; use aleo_std::prelude::{finish, lap, timer}; use indexmap::{IndexMap, IndexSet}; From 7b1d21dada94770b251f66fd48d0c7ed69517131 Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Sun, 27 Oct 2024 20:28:15 +0100 Subject: [PATCH 10/11] DRY up compute spend checks using compute_cost_in_microcredits helper fn --- console/network/src/lib.rs | 6 +- ledger/src/tests.rs | 4 +- synthesizer/process/src/cost.rs | 50 +++++-- .../process/src/stack/helpers/initialize.rs | 2 +- synthesizer/process/src/stack/mod.rs | 2 +- synthesizer/src/vm/execute.rs | 6 +- synthesizer/src/vm/finalize.rs | 128 ++++++------------ synthesizer/src/vm/mod.rs | 2 +- 8 files changed, 94 insertions(+), 106 deletions(-) diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 8c383d3c6d..6ff31cd45d 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -133,9 +133,9 @@ pub trait Network: const MAX_FEE: u64 = 1_000_000_000_000_000; /// The maximum number of microcredits that can be spent on a finalize block. const TRANSACTION_SPEND_LIMIT: u64 = 100_000_000; - /// The fixed cost in microcredits to verify an execution. - // NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. - const EXECUTION_FIXED_COST: u64 = 2_000_000; // 2 million microcredits + /// The base cost in microcredits to verify an execution. + /// NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. + const EXECUTION_BASE_COST: u64 = 2_000_000; // 2 million microcredits /// The maximum number of microcredits that can be spent in a block. const BLOCK_SPEND_LIMIT: u64 = 950_000_000; diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index f10c950f01..1b404e1797 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -34,7 +34,7 @@ use ledger_store::{ConsensusStore, helpers::memory::ConsensusMemory}; use snarkvm_utilities::try_vm_runtime; use synthesizer::{ Stack, - process::synthesis_cost, + process::deployment_synthesis_cost, program::{Program, StackProgram}, vm::VM, }; @@ -3263,7 +3263,7 @@ fn test_deployments_exceed_block_spend_limit() { let deployment = ledger.vm().deploy(&private_key, &program, None, 0, None, rng).unwrap(); // Get the synthesis cost. - let synthesis_cost = synthesis_cost(deployment.deployment().unwrap()).unwrap(); + let synthesis_cost = deployment_synthesis_cost(deployment.deployment().unwrap()).unwrap(); // Determine number of deployments that cannot be included in a block. let num_deployments = usize::try_from(::BLOCK_SPEND_LIMIT / synthesis_cost + 1).unwrap(); diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 73a09d72a7..dcbe443d82 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -13,13 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::sync::Arc; + use crate::{Process, Stack, StackProgramTypes}; use console::{ prelude::*, program::{FinalizeType, Identifier, LiteralType, PlaintextType}, }; -use ledger_block::{Deployment, Execution}; +use ledger_block::{Deployment, Execution, Transaction}; use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, StackProgram}; /// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). @@ -37,7 +39,7 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; // Compute the synthesis cost in microcredits. - let synthesis_cost = synthesis_cost(deployment)?; + let synthesis_cost = deployment_synthesis_cost(deployment)?; // Compute the namespace cost in credits: 10^(10 - num_characters). let namespace_cost = 10u64 @@ -55,7 +57,7 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( } /// Returns the cost in microcredits to synthesize a deployment. -pub fn synthesis_cost(deployment: &Deployment) -> Result { +pub fn deployment_synthesis_cost(deployment: &Deployment) -> Result { let num_combined_variables = deployment.num_combined_variables()?; let num_combined_constraints = deployment.num_combined_constraints()?; Ok(num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER) @@ -89,12 +91,6 @@ fn execution_storage_cost(size_in_bytes: u64) -> u64 { } } -/// Returns the fixed cost for an execution. -/// NOTE: this constant reflects the compute cost of an execution, but is not required to be paid by the user. -pub const fn execution_fixed_cost() -> u64 { - N::EXECUTION_FIXED_COST -} - /// Finalize costs for compute heavy operations, derived as: /// `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`. @@ -177,7 +173,11 @@ fn cost_in_size<'a, N: Network>( } /// Returns the the cost of a command in a finalize scope. -pub fn cost_per_command(stack: &Stack, finalize: &Finalize, command: &Command) -> Result { +pub fn finalize_cost_per_command( + stack: &Stack, + finalize: &Finalize, + command: &Command, +) -> Result { match command { Command::Instruction(Instruction::Abs(_)) => Ok(500), Command::Instruction(Instruction::AbsWrapped(_)) => Ok(500), @@ -376,7 +376,7 @@ pub fn cost_per_command(stack: &Stack, finalize: &Finalize, co } /// Returns the minimum number of microcredits required to run the finalize. -pub fn cost_in_microcredits(stack: &Stack, function_name: &Identifier) -> Result { +pub fn finalize_cost_in_microcredits(stack: &Stack, function_name: &Identifier) -> Result { // Retrieve the finalize logic. let Some(finalize) = stack.get_function_ref(function_name)?.finalize_logic() else { // Return a finalize cost of 0, if the function does not have a finalize scope. @@ -398,12 +398,38 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi finalize .commands() .iter() - .map(|command| cost_per_command(stack, finalize, command)) + .map(|command| finalize_cost_per_command(stack, finalize, command)) .try_fold(future_cost, |acc, res| { res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) }) } +/// Returns the compute cost for a transaction. +/// This allows the VM to determine whether a transaction surpasses the BLOCK_SPEND_LIMIT. +/// This does NOT represent the full costs which a user has to pay. +pub fn compute_cost_in_microcredits( + transaction: &Transaction, + stack: &Option>>, +) -> Result { + match transaction { + // Deploy transaction compute is dominated by synthesis costs. + Transaction::Deploy(_, _, deployment, _) => deployment_synthesis_cost(deployment), + // Execute transaction compute is dominated by a base cost and finalize costs. + Transaction::Execute(_, execution, _) => { + // Get the root transition. + let root_transition = execution.peek()?; + // Ensure that a stack was provided. + let stack = stack.as_ref().ok_or(anyhow!("Expected a Stack containing the Execution's finalize cost."))?; + // Get the finalize cost from the process. + let finalize_cost = stack.get_finalize_cost(root_transition.function_name())?; + // Add the base execution cost. + Ok(finalize_cost.saturating_add(N::EXECUTION_BASE_COST)) + } + // Fee transactions cannot be sent to the VM by outside parties so do not have a compute cost. + Transaction::Fee(..) => Ok(0), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/synthesizer/process/src/stack/helpers/initialize.rs b/synthesizer/process/src/stack/helpers/initialize.rs index ea1d368ec0..4374be9f51 100644 --- a/synthesizer/process/src/stack/helpers/initialize.rs +++ b/synthesizer/process/src/stack/helpers/initialize.rs @@ -86,7 +86,7 @@ impl Stack { stack.number_of_calls.insert(*function.name(), num_calls); // Get the finalize cost. - let finalize_cost = cost_in_microcredits(&stack, function.name())?; + let finalize_cost = finalize_cost_in_microcredits(&stack, function.name())?; // Check that the finalize cost does not exceed the maximum. ensure!( finalize_cost <= N::TRANSACTION_SPEND_LIMIT, diff --git a/synthesizer/process/src/stack/mod.rs b/synthesizer/process/src/stack/mod.rs index 5b3e627424..8bfd2014bc 100644 --- a/synthesizer/process/src/stack/mod.rs +++ b/synthesizer/process/src/stack/mod.rs @@ -37,7 +37,7 @@ mod evaluate; mod execute; mod helpers; -use crate::{CallMetrics, Process, Trace, cost_in_microcredits, traits::*}; +use crate::{CallMetrics, Process, Trace, finalize_cost_in_microcredits, traits::*}; use console::{ account::{Address, PrivateKey}, network::prelude::*, diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 1b6d54e5f4..e8b02d16b2 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -216,7 +216,7 @@ mod tests { }; use ledger_block::Transition; use ledger_store::helpers::memory::ConsensusMemory; - use synthesizer_process::cost_per_command; + use synthesizer_process::finalize_cost_per_command; use synthesizer_program::StackProgram; use indexmap::IndexMap; @@ -753,7 +753,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| cost_per_command(&stack, finalize_logic, command)) + .map(|command| finalize_cost_per_command(&stack, finalize_logic, command)) .try_fold(0u64, |acc, res| { res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) }) @@ -888,7 +888,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| cost_per_command(&stack, finalize_logic, command)) + .map(|command| finalize_cost_per_command(&stack, finalize_logic, command)) .try_fold(0u64, |acc, res| { res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) }) diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 134074c62e..a9ba07ff9d 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -333,7 +333,7 @@ impl> VM { // Initialize seen transaction details. let mut tx_details = SeenTransactionDetails::new(); // Initialize a counter for the total amount of microcredits spent on compute. - let mut microcredits_spent_on_compute = 0u64; + let mut total_compute_spend = 0u64; // Finalize the transactions. 'outer: for transaction in transactions { @@ -346,9 +346,19 @@ impl> VM { continue 'outer; } + // Collect the Optional Stack corresponding to the transaction. + let stack = if let Transaction::Execute(_, execution, _) = transaction { + // Get the root transition from the execution. + let root_transition = execution.peek().map_err(|e| e.to_string())?; + // Get the stack from the process. + Some(process.get_stack(root_transition.program_id()).map_err(|e| e.to_string())?.clone()) + } else { + None + }; + // Determine if the transaction should be aborted. if let Some(reason) = - self.should_abort_transaction(&process, transaction, &tx_details, microcredits_spent_on_compute) + self.should_abort_transaction(transaction, &tx_details, total_compute_spend, &stack) { // Store the aborted transaction. aborted.push((transaction.clone(), reason)); @@ -474,28 +484,12 @@ impl> VM { Ok(confirmed_transaction) => { // Add the transaction details to the seen transaction details. tx_details.insert_transaction_details(confirmed_transaction.transaction()); - // Add any synthesis cost to the total microcredits spent on compute. - if let Transaction::Deploy(_, _, deployment, _) = confirmed_transaction.transaction() { - // Compute the synthesis cost. - let synthesis_cost = synthesis_cost(deployment).map_err(|e| e.to_string())?; - // Add the synthesis cost to the total microcredits spent on compute. - microcredits_spent_on_compute = - microcredits_spent_on_compute.saturating_add(synthesis_cost); - } - // Add any finalize cost to the total finalize cost. - if let Transaction::Execute(_, execution, _) = confirmed_transaction.transaction() { - // Get the root transition from the execution. - let root_transition = execution.peek().map_err(|e| e.to_string())?; - // Get the finalize cost from the process. - let finalize_cost = process - .get_finalize_cost(root_transition.program_id(), root_transition.function_name()) + // Compute the transaction cost. + let tx_compute_cost = + compute_cost_in_microcredits::(confirmed_transaction.transaction(), &stack) .map_err(|e| e.to_string())?; - // Add the fixed execution cost. - let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); - // Add the execution cost to the total microcredits spent on compute. - microcredits_spent_on_compute = - microcredits_spent_on_compute.saturating_add(execution_cost); - } + // Add the transaction cost to the total microcredits spent on compute. + total_compute_spend = total_compute_spend.saturating_add(tx_compute_cost); // Store the confirmed transaction. confirmed.push(confirmed_transaction); // Increment the transaction index counter. @@ -826,10 +820,10 @@ impl> VM { /// - The transaction exceeds the block spend limit. fn should_abort_transaction( &self, - process: &Process, transaction: &Transaction, tx_details: &SeenTransactionDetails, - microcredits_spent_on_compute: u64, + total_compute_spend: u64, + stack: &Option>>, ) -> Option { // Get the current block height to help determine which checks to perform. let current_block_height = self.block_store().current_block_height(); @@ -874,44 +868,24 @@ impl> VM { } // If the transaction is a deployment, ensure that it is not another deployment in the block from the same public fee payer. - if let Transaction::Deploy(_, _, deployment, fee) = transaction { + if let Transaction::Deploy(_, _, _, fee) = transaction { // If any public deployment payer has already deployed in this block, abort the transaction. if let Some(payer) = fee.payer() { if tx_details.deployment_payers().contains(&payer) { return Some(format!("Another deployment in the block from the same public fee payer {payer}")); } } - // Activate deployment checks from CONSENSUS_V2_HEIGHT onwards. - if current_block_height >= N::CONSENSUS_V2_HEIGHT { - // Check that the synthesis cost does not exceed the maximum. - let Ok(synthesis_cost) = synthesis_cost(deployment) else { - return Some("Failed to get synthesis cost".to_string()); - }; - // If the synthesis cost exceeds the block spend limit, abort the transaction. - if synthesis_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { - return Some("Exceeds block spend limit".to_string()); - } - } } - // If the transaction is an execution, ensure that the total finalize cost does not exceed the block spend limit. - if let Transaction::Execute(_, execution, _) = transaction { - // Activate execute checks from CONSENSUS_V2_HEIGHT onwards. - if current_block_height >= N::CONSENSUS_V2_HEIGHT { - // Get the root transition from the execution. - let Ok(root) = execution.peek() else { - return Some("Failed to get root transition".to_string()); - }; - // Get the finalize cost from the process. - let Ok(finalize_cost) = process.get_finalize_cost(root.program_id(), root.function_name()) else { - return Some("Failed to get finalize cost".to_string()); - }; - // Add the fixed execution cost. - let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); - // If the execution cost exceeds the block spend limit, abort the transaction. - if execution_cost.saturating_add(microcredits_spent_on_compute) > N::BLOCK_SPEND_LIMIT { - return Some("Exceeds block spend limit".to_string()); - } + // Activate compute spend check from CONSENSUS_V2_HEIGHT onwards. + if current_block_height >= N::CONSENSUS_V2_HEIGHT { + // Compute the transaction compute cost. + let Ok(tx_compute_cost) = compute_cost_in_microcredits::(transaction, stack) else { + return Some("Failed to compute the transaction compute cost".to_string()); + }; + // If the transaction compute cost exceeds the block spend limit, abort the transaction. + if tx_compute_cost.saturating_add(total_compute_spend) > N::BLOCK_SPEND_LIMIT { + return Some("Exceeds block spend limit".to_string()); } } @@ -938,7 +912,7 @@ impl> VM { // Initialize seen transaction details. let mut tx_details = SeenTransactionDetails::new(); // Initialize a counter for the total amount of microcredits spent on compute. - let mut microcredits_spent_on_compute = 0u64; + let mut total_compute_spend = 0u64; // Abort the transactions that are have duplicates or are invalid. This will prevent the VM from performing // verification on transactions that would have been aborted in `VM::atomic_speculate`. @@ -949,40 +923,28 @@ impl> VM { continue; } + // Collect the Optional Stack corresponding to the transaction if its an Execution. + let stack = if let Transaction::Execute(_, execution, _) = transaction { + // Get the root transition from the execution. + let root_transition = execution.peek()?; + // Get the stack from the process. + Some(self.process.read().get_stack(root_transition.program_id())?.clone()) + } else { + None + }; + // Determine if the transaction should be aborted. - match self.should_abort_transaction( - &self.process.read(), - transaction, - &tx_details, - microcredits_spent_on_compute, - ) { + match self.should_abort_transaction(transaction, &tx_details, total_compute_spend, &stack) { // Store the aborted transaction. Some(reason) => aborted_transactions.push((*transaction, reason.to_string())), // Track the transaction state. None => { // Add the transaction details to the seen transaction details. tx_details.insert_transaction_details(*transaction); - - if let Transaction::Deploy(_, _, deployment, _) = transaction { - // Compute the synthesis cost. - let synthesis_cost = synthesis_cost(deployment)?; - // Add the synthesis cost to the total microcredits spent on compute. - microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(synthesis_cost); - } - // Add any finalize cost to the total finalize cost. - if let Transaction::Execute(_, execution, _) = transaction { - // Get the root transition from the execution. - let root_transition = execution.peek()?; - // Get the finalize cost from the process. - let finalize_cost = self - .process - .read() - .get_finalize_cost(root_transition.program_id(), root_transition.function_name())?; - // Add the fixed execution cost. - let execution_cost = finalize_cost.saturating_add(execution_fixed_cost::()); - // Add the execution cost to the total microcredits spent on compute. - microcredits_spent_on_compute = microcredits_spent_on_compute.saturating_add(execution_cost); - } + // Compute the transaction cost. + let tx_compute_cost = compute_cost_in_microcredits::(transaction, &stack)?; + // Add the transaction cost to the total microcredits spent on compute. + total_compute_spend = total_compute_spend.saturating_add(tx_compute_cost); // Add the transaction to the list of transactions to verify. transactions_to_verify.push(transaction); diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 76608d229f..16e032bc47 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -59,7 +59,7 @@ use ledger_store::{ TransitionStore, atomic_finalize, }; -use process::{Authorization, Process, Trace, deployment_cost, execution_cost, execution_fixed_cost, synthesis_cost}; +use process::{Authorization, Process, Stack, Trace, compute_cost_in_microcredits, deployment_cost, execution_cost}; use synthesizer_program::{FinalizeGlobalState, FinalizeOperation, FinalizeStoreTrait, Program}; use utilities::{cfg_sort_by_cached_key, try_vm_runtime}; From b898eec93f4b99ab7f95d787fb89a19d5599c43a Mon Sep 17 00:00:00 2001 From: Victor Sint Nicolaas Date: Mon, 28 Oct 2024 08:22:10 +0100 Subject: [PATCH 11/11] Lower MAXIMUM_CONFIRMED_TRANSACTIONS in testing, because we can --- synthesizer/src/vm/finalize.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index a9ba07ff9d..48e0c83a0d 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -230,9 +230,9 @@ impl> VM { #[cfg(not(any(test, feature = "test")))] pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = Transactions::::MAX_TRANSACTIONS; /// The maximum number of confirmed transactions allowed in a block. - /// This is deliberately set to a low value (8) for testing purposes only. + /// This is deliberately set to a low value (32) for testing purposes only. #[cfg(any(test, feature = "test"))] - pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = 256; + pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = 32; /// Performs atomic speculation over a list of transactions. ///