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
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions console/network/src/canary_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 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;
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
7 changes: 7 additions & 0 deletions console/network/src/mainnet_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions console/network/src/testnet_v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
206 changes: 205 additions & 1 deletion ledger/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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..<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.
// If we succeed, the finalize cost must be below the TRANSACTION_SPEND_LIMIT.
if let Ok(stack) = Stack::<CurrentNetwork>::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..(<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_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(<CurrentNetwork as Network>::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::<CurrentNetwork>::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..(<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(&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();
}
2 changes: 1 addition & 1 deletion ledger/store/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ impl<N: Network, B: BlockStorage<N>> BlockStore<N, B> {

/// 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`.
Expand Down
Loading