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 f0a973acd9..990dab48d1 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -133,6 +133,13 @@ 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. + #[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/lib.rs b/console/network/src/lib.rs index 099044f171..6ff31cd45d 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -133,6 +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 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; /// 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; @@ -201,6 +206,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..0142cf9045 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -134,6 +134,13 @@ 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. + #[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 fd793055a4..e841798d49 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -133,6 +133,13 @@ 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. + #[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/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..1b404e1797 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, program::Program, vm::VM}; +use synthesizer::{ + Stack, + process::deployment_synthesis_cost, + program::{Program, StackProgram}, + vm::VM, +}; use indexmap::{IndexMap, IndexSet}; use rand::seq::SliceRandom; @@ -3142,3 +3147,202 @@ 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_executions_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. + // 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; + } + } + + // 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_deployments_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 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 = 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(); + + // 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(); + + // 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; + function foo: + input r0 as [field; 20u32].private; + hash.sha3_256 r0 into r1 as field;", + i + 1 + )) + .unwrap(); + + 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 = deployment_transactions.len(); + + // Construct the next block. + 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); + 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/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`. diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 9be8228648..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)). @@ -30,10 +32,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 +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 = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER; + let synthesis_cost = deployment_synthesis_cost(deployment)?; // Compute the namespace cost in credits: 10^(10 - num_characters). let namespace_cost = 10u64 @@ -58,6 +56,13 @@ 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 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) +} + /// 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. @@ -168,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), @@ -367,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. @@ -389,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/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/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 2cbef6359a..48e0c83a0d 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -15,8 +15,62 @@ use super::*; -use ledger_committee::{MAX_DELEGATORS, MIN_DELEGATOR_STAKE, MIN_VALIDATOR_SELF_STAKE}; -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. @@ -176,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 = 8; + pub const MAXIMUM_CONFIRMED_TRANSACTIONS: usize = 32; /// Performs atomic speculation over a list of transactions. /// @@ -276,16 +330,10 @@ 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 total_compute_spend = 0u64; // Finalize the transactions. 'outer: for transaction in transactions { @@ -298,15 +346,20 @@ 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( - transaction, - &transition_ids, - &input_ids, - &output_ids, - &tpks, - &deployment_payers, - ) { + if let Some(reason) = + self.should_abort_transaction(transaction, &tx_details, total_compute_spend, &stack) + { // Store the aborted transaction. aborted.push((transaction.clone(), reason)); // Continue to the next transaction. @@ -429,18 +482,14 @@ 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()); - // Add any public deployment payer to the set of deployment payers. - if let Transaction::Deploy(_, _, _, fee) = confirmed_transaction.transaction() { - 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()); + // Compute the transaction cost. + let tx_compute_cost = + compute_cost_in_microcredits::(confirmed_transaction.transaction(), &stack) + .map_err(|e| e.to_string())?; + // 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. @@ -768,19 +817,21 @@ 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 exceeds the block spend limit. fn should_abort_transaction( &self, transaction: &Transaction, - transition_ids: &IndexSet, - input_ids: &IndexSet>, - output_ids: &IndexSet>, - tpks: &IndexSet>, - deployment_payers: &IndexSet>, + tx_details: &SeenTransactionDetails, + 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(); + // 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}")); @@ -790,7 +841,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}")); } } @@ -798,7 +851,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}")); } } @@ -807,7 +862,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}")); } } @@ -816,12 +871,24 @@ impl> VM { 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 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}")); } } } + // 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()); + } + } + // Return `None` because the transaction is well-formed. None } @@ -842,16 +909,10 @@ 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 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`. @@ -862,31 +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( - transaction, - &transition_ids, - &input_ids, - &output_ids, - &tpks, - &deployment_payers, - ) { + 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 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()); - // Add any public deployment payer to the set of deployment payers. - if let Transaction::Deploy(_, _, _, fee) = transaction { - fee.payer().map(|payer| deployment_payers.insert(payer)); - } + // Add the transaction details to the seen transaction details. + tx_details.insert_transaction_details(*transaction); + // 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); @@ -1569,7 +1627,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 diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 0be0f85440..16e032bc47 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, Stack, Trace, compute_cost_in_microcredits, deployment_cost, execution_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};