Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Introduce BLOCK_SPEND_LIMIT #2565

Draft
wants to merge 11 commits into
base: staging
Choose a base branch
from
3 changes: 3 additions & 0 deletions console/network/src/canary_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions console/network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 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;
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions console/network/src/mainnet_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions console/network/src/testnet_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 49 additions & 9 deletions ledger/benches/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<MainnetV0>::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::<MainnetV0>::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::<MainnetV0>::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())
});
}
Expand Down
167 changes: 166 additions & 1 deletion ledger/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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..<CurrentNetwork as Network>::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::<Vec<_>>().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::<CurrentNetwork>::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 > <CurrentNetwork as Network>::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..(<CurrentNetwork as Network>::BLOCK_SPEND_LIMIT / finalize_cost + 1) {
transactions.push(
ledger
.vm()
.execute(
&private_key,
(program.id(), "foo"),
Vec::<Value<CurrentNetwork>>::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_spend_limit_deployments() {
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..(<CurrentNetwork as Network>::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();
}
19 changes: 14 additions & 5 deletions synthesizer/process/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,14 @@ pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> 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
.checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER)
.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
Expand All @@ -58,6 +54,13 @@ pub fn deployment_cost<N: Network>(deployment: &Deployment<N>) -> Result<(u64, (
Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost)))
}

/// Returns the cost in microcredits to synthesize a deployment.
pub fn synthesis_cost<N: Network>(deployment: &Deployment<N>) -> Result<u64> {
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<N: Network>(process: &Process<N>, execution: &Execution<N>) -> Result<(u64, (u64, u64))> {
// Compute the storage cost in microcredits.
Expand Down Expand Up @@ -86,6 +89,12 @@ fn execution_storage_cost<N: Network>(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<N: Network>() -> u64 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there plans to extend this logic (in which case an alternative name might be more fitting)? I'm wondering about the rationale for the existence of a (non-const) function wrapper for a const

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I swear this was in my git staging area already: 3443c3e

are there plans to extend this logic (in which case an alternative name might be more fitting)?

Unclear at the moment. Only other name I can imagine is execution_compute_cost?

N::EXECUTION_FIXED_COST
}

/// Finalize costs for compute heavy operations, derived as:
/// `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`.

Expand Down
15 changes: 15 additions & 0 deletions synthesizer/process/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ impl<N: Network> Process<N> {
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<ProgramID<N>>,
function_name: impl TryInto<Identifier<N>>,
) -> Result<u64> {
// 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(
Expand Down
Loading