diff --git a/executor/codegen/data/internal-constants.json b/executor/codegen/data/internal-constants.json index 4c2c826b..d2b2a50c 100644 --- a/executor/codegen/data/internal-constants.json +++ b/executor/codegen/data/internal-constants.json @@ -23,6 +23,7 @@ "repr": "u32", "values": { "nondet_blocks": 4096, + "default_allocation_tree_depth": 5, "locked_slots": 256, "upgraders": 32, "vm_recursion": 512, diff --git a/executor/crates/common/src/internal_constants.rs b/executor/crates/common/src/internal_constants.rs index cabc1335..36b9c893 100644 --- a/executor/crates/common/src/internal_constants.rs +++ b/executor/crates/common/src/internal_constants.rs @@ -18,6 +18,7 @@ pub mod memory_limiter_consts { pub mod top_limits { pub const NONDET_BLOCKS: u32 = 4096; + pub const DEFAULT_ALLOCATION_TREE_DEPTH: u32 = 5; pub const LOCKED_SLOTS: u32 = 256; pub const UPGRADERS: u32 = 32; pub const VM_RECURSION: u32 = 512; diff --git a/executor/install/config/genvm.yaml b/executor/install/config/genvm.yaml index 2d4c3427..30e7878f 100644 --- a/executor/install/config/genvm.yaml +++ b/executor/install/config/genvm.yaml @@ -238,8 +238,7 @@ fees: # reject only a non-zero per-round budget below the floor (matches the # chain's `execBudget > 0 && execBudget < floor`) let budgetTooLow = - if a.skipBudgetFloor then false - else if a.matchedFeeParams.executionBudgetPerRound > 0 + if a.matchedFeeParams.executionBudgetPerRound > 0 then a.matchedFeeParams.executionBudgetPerRound < budgetFloor else false in if a.isInternal diff --git a/executor/src/lib.rs b/executor/src/lib.rs index ef1db177..2db47d69 100644 --- a/executor/src/lib.rs +++ b/executor/src/lib.rs @@ -470,6 +470,7 @@ pub async fn run_with_impl( }; let data_fees_limit = supervisor.shared_data.gep(|x| &x.data_fees_limit); + let message_fee_cascade = entry_data.message_fee_cascade; let essential_data = Box::new(wasi::genlayer_sdk::SingleVMData { limiter: limiter_det, @@ -515,7 +516,11 @@ pub async fn run_with_impl( granted_custom: Vec::new(), }); - let run_result = rt::spawn_apply_run(&supervisor, essential_data).await?; + let mut run_result = rt::spawn_apply_run(&supervisor, essential_data).await?; + + if let Some(cascade) = &message_fee_cascade { + rt::cascade::apply(&mut run_result.vm_data.accumulator.emissions, cascade); + } if let Some(code) = extra_leader_nondet_output_error(&supervisor, &run_result.run_ok) { return Ok(rt::vm::FullResult::empty_from(rt::vm::RunOk::FatalVMError( diff --git a/executor/src/rt/cascade.rs b/executor/src/rt/cascade.rs new file mode 100644 index 00000000..3ff59557 --- /dev/null +++ b/executor/src/rt/cascade.rs @@ -0,0 +1,60 @@ +use primitive_types::U256; + +use crate::domain::ExecutionEmission; + +fn pool_funded_fee(emission: &mut ExecutionEmission) -> Option<&mut U256> { + match emission { + ExecutionEmission::InternalMessage { + message_fee, + use_balance: false, + .. + } + | ExecutionEmission::InternalDeployMessage { + message_fee, + use_balance: false, + .. + } => Some(message_fee), + _ => None, + } +} + +/// Raises each sender-pool internal message to the floor, then adds an equal +/// share of half the remaining budget after fees and external reserves; the +/// other half stays with the parent so one deep path cannot drain the tree +pub fn apply( + emissions: &mut [ExecutionEmission], + cascade: &genvm_modules_interfaces::fees::MessageFeeCascade, +) { + let mut sum = emissions.iter().fold(U256::zero(), |sum, emission| { + if matches!(emission, ExecutionEmission::ExternalMessage { .. }) { + sum.saturating_add(cascade.external_fee_reserve) + } else { + sum + } + }); + let mut count = 0u64; + + for fee in emissions.iter_mut().filter_map(pool_funded_fee) { + *fee = (*fee).max(cascade.min_primary_fee); + sum = sum.saturating_add(*fee); + count += 1; + } + + if count == 0 || sum >= cascade.budget_remaining { + return; + } + + let per_child = (cascade.budget_remaining - sum) / (U256::from(2) * U256::from(count)); + if per_child.is_zero() { + return; + } + + // Each fee is at most `sum`, and `per_child` at most `budget_remaining - sum` + for fee in emissions.iter_mut().filter_map(pool_funded_fee) { + *fee += per_child; + } +} + +#[cfg(test)] +#[path = "cascade_test.rs"] +mod tests; diff --git a/executor/src/rt/cascade_test.rs b/executor/src/rt/cascade_test.rs new file mode 100644 index 00000000..9e71b9b1 --- /dev/null +++ b/executor/src/rt/cascade_test.rs @@ -0,0 +1,139 @@ +use super::*; +use genlayer_sdk::abi; + +fn internal(message_fee: u64, use_balance: bool) -> ExecutionEmission { + ExecutionEmission::InternalMessage { + call_key: abi::CallKey([0; 32]), + address: genlayer_sdk::calldata::Address::zero(), + calldata: abi::entry::MainCallData { + name: None, + args: None, + kwargs: None, + }, + value: U256::zero(), + on: abi::gl_call::On::Finalized, + message_fee: U256::from(message_fee), + receipt_fee: U256::from(7), + fee_params: abi::fees::InternalMessageParams { + leader_time_units_allocation: U256::one(), + validator_time_units_allocation: U256::one(), + execution_budget_per_round: U256::one(), + rotations: vec![U256::one()], + max_price_gen_per_time_unit: U256::one(), + storage_fee_max_gas_price: U256::one(), + receipt_fee_max_gas_price: U256::one(), + }, + subtree: bytes::Bytes::new(), + use_balance, + } +} + +fn external(message_fee: u64) -> ExecutionEmission { + ExecutionEmission::ExternalMessage { + address: genlayer_sdk::calldata::Address::zero(), + calldata: bytes::Bytes::new(), + value: U256::zero(), + message_fee: U256::from(message_fee), + receipt_fee: U256::zero(), + fee_params: abi::fees::ExternalMessageParams { + gas_limit: U256::one(), + max_gas_price: U256::one(), + }, + } +} + +fn cascade( + min_primary_fee: u64, + budget_remaining: u64, +) -> genvm_modules_interfaces::fees::MessageFeeCascade { + genvm_modules_interfaces::fees::MessageFeeCascade { + min_primary_fee: U256::from(min_primary_fee), + budget_remaining: U256::from(budget_remaining), + external_fee_reserve: U256::zero(), + } +} + +fn fees(emissions: &[ExecutionEmission]) -> Vec { + emissions + .iter() + .map(|e| match e { + ExecutionEmission::InternalMessage { message_fee, .. } + | ExecutionEmission::InternalDeployMessage { message_fee, .. } + | ExecutionEmission::ExternalMessage { message_fee, .. } => message_fee.low_u64(), + ExecutionEmission::Event { .. } => 0, + }) + .collect() +} + +#[test] +fn delivered_child_reduces_the_next_generation_split() { + let mut emissions = vec![internal(0, false)]; + apply(&mut emissions, &cascade(2, 400)); + assert_eq!(fees(&emissions), vec![201]); + let mut replacement = vec![internal(0, false)]; + apply(&mut replacement, &cascade(2, 199)); + assert_eq!(fees(&replacement), vec![100]); +} + +#[test] +fn reserves_each_external_envelope_before_splitting() { + let mut emissions = vec![internal(10, false), external(1), external(1)]; + let mut params = cascade(30, 1_000); + params.external_fee_reserve = U256::from(400); + apply(&mut emissions, ¶ms); + assert_eq!(fees(&emissions), vec![115, 1, 1]); +} + +#[test] +fn insufficient_remaining_budget_still_applies_floors() { + let mut emissions = vec![internal(10, false), internal(10, false), external(1)]; + let mut params = cascade(30, 20); + params.external_fee_reserve = U256::MAX; + apply(&mut emissions, ¶ms); + assert_eq!(fees(&emissions), vec![30, 30, 1]); +} + +#[test] +fn floors_then_splits_half_of_spare() { + let mut emissions = vec![internal(10, false), internal(40, false), external(5)]; + apply(&mut emissions, &cascade(30, 1_000)); + // floors 30 + 40 = 70, spare 930, per child 930 / 4 = 232 + assert_eq!(fees(&emissions), vec![262, 272, 5]); +} + +#[test] +fn balance_funded_messages_are_neither_floored_nor_counted() { + let mut emissions = vec![internal(10, true), internal(10, false)]; + apply(&mut emissions, &cascade(30, 130)); + // spare 100 over 1 child + assert_eq!(fees(&emissions), vec![10, 80]); +} + +#[test] +fn no_spare_only_floors() { + let mut emissions = vec![internal(10, false), internal(10, false)]; + apply(&mut emissions, &cascade(30, 60)); + assert_eq!(fees(&emissions), vec![30, 30]); +} + +#[test] +fn share_rounding_to_zero_only_floors() { + let mut emissions = vec![internal(30, false), internal(30, false)]; + apply(&mut emissions, &cascade(30, 63)); + assert_eq!(fees(&emissions), vec![30, 30]); +} + +#[test] +fn saturated_sum_is_not_spare() { + let mut emissions = vec![internal(0, false), internal(0, false)]; + let mut big = cascade(0, 0); + big.min_primary_fee = U256::MAX; + big.budget_remaining = U256::MAX; + apply(&mut emissions, &big); + for e in &emissions { + let ExecutionEmission::InternalMessage { message_fee, .. } = e else { + unreachable!() + }; + assert_eq!(*message_fee, U256::MAX); + } +} diff --git a/executor/src/rt/fees.rs b/executor/src/rt/fees.rs index 90cb6a6b..f361a9c4 100644 --- a/executor/src/rt/fees.rs +++ b/executor/src/rt/fees.rs @@ -264,6 +264,8 @@ pub struct MessageReceiptParams { #[derive(Debug)] pub struct DataLimit { + pub max_allocation_tree_depth: u32, + pub min_external_gas_limit: primitive_types::U256, buckets: tokio::sync::Mutex>, storage: Bucket, message_receipt: Bucket, @@ -278,6 +280,19 @@ impl DataLimit { fees: crate::config::FeesConfig, gas_data: std::collections::BTreeMap, ) -> rt::errors::Result { + let max_allocation_tree_depth = gas_data + .get("maxAllocationTreeDepth") + .map(|value| primitive_types::U256::from_dec_str(value)) + .transpose() + .map_err(|e| rt::errors::internal!("parsing maxAllocationTreeDepth: {e}"))? + .map(|value| value.min(primitive_types::U256::from(u32::MAX)).low_u32()) + .unwrap_or(genvm_common::internal_constants::top_limits::DEFAULT_ALLOCATION_TREE_DEPTH); + let min_external_gas_limit = gas_data + .get("minExternalGasLimit") + .map(|value| primitive_types::U256::from_dec_str(value)) + .transpose() + .map_err(|e| rt::errors::internal!("parsing minExternalGasLimit: {e}"))? + .unwrap_or_default(); let prelude = &fees.expr_prelude; // Host-provided values are exposed under a single `node` object, e.g. @@ -343,6 +358,8 @@ impl DataLimit { } Ok(Self { + max_allocation_tree_depth, + min_external_gas_limit, buckets: tokio::sync::Mutex::new(bucket_totals), storage, message_receipt, @@ -606,27 +623,11 @@ impl DataLimit { pub fn calculate_message_fee_internal( &self, matched_fee_params: &genlayer_sdk::abi::fees::InternalMessageParams, - ) -> rt::errors::Result { - self.calculate_message_fee_internal_impl(matched_fee_params, false) - } - - pub fn calculate_descendant_message_fee_internal( - &self, - matched_fee_params: &genlayer_sdk::abi::fees::InternalMessageParams, - ) -> rt::errors::Result { - self.calculate_message_fee_internal_impl(matched_fee_params, true) - } - - fn calculate_message_fee_internal_impl( - &self, - matched_fee_params: &genlayer_sdk::abi::fees::InternalMessageParams, - skip_budget_floor: bool, ) -> rt::errors::Result { self.calculate_bucket( &self.message_fee, &[ ("isInternal", true.into()), - ("skipBudgetFloor", skip_budget_floor.into()), ( "matchedFeeParams", fee_params_value_internal(matched_fee_params), diff --git a/executor/src/rt/mod.rs b/executor/src/rt/mod.rs index 02ca2bfc..dbb6d293 100644 --- a/executor/src/rt/mod.rs +++ b/executor/src/rt/mod.rs @@ -1,3 +1,4 @@ +pub mod cascade; pub mod errors; pub mod fees; pub mod memlimiter; diff --git a/executor/src/wasi/genlayer_sdk/descendant_grant.rs b/executor/src/wasi/genlayer_sdk/descendant_grant.rs index 85796011..92fd63f5 100644 --- a/executor/src/wasi/genlayer_sdk/descendant_grant.rs +++ b/executor/src/wasi/genlayer_sdk/descendant_grant.rs @@ -173,6 +173,8 @@ fn sibling_key(node: &abi::gl_call::AllocationNode) -> (u8, [u8; 20], [u8; 32]) fn validate_nodes( nodes: &[abi::gl_call::AllocationNode], enclosing_rotations: usize, + max_depth: u32, + min_external_gas_limit: U256, quote: &mut F, ) -> Result where @@ -183,6 +185,7 @@ where } let mut budget = U256::zero(); let mut children_budgets = vec![U256::zero(); nodes.len()]; + let mut depths = vec![0u32; nodes.len()]; let mut sibling_keys = BTreeSet::new(); for (index, node) in nodes.iter().enumerate() { let (parent_index, node_budget) = match node { @@ -196,6 +199,7 @@ where return Err(GrantValidationError::Tree); } let total = if parent_index == ROOT_PARENT { + depths[index] = 1; &mut budget } else { if parent_index >= U256::from(index) @@ -207,8 +211,12 @@ where { return Err(GrantValidationError::Tree); } + depths[index] = depths[parent_index.as_usize()] + 1; &mut children_budgets[parent_index.as_usize()] }; + if depths[index] > max_depth { + return Err(GrantValidationError::Tree); + } *total = total .checked_add(node_budget) .ok_or(GrantValidationError::Budget)?; @@ -258,7 +266,9 @@ where } } abi::gl_call::AllocationNode::External(node) => { - // The transaction-pinned minimum external gas limit is not available in host input yet. + if node.fee_params.gas_limit < min_external_gas_limit { + return Err(GrantValidationError::External); + } let unit = node .fee_params .gas_limit @@ -289,6 +299,8 @@ where pub(super) fn prepare_descendants( enclosing_params: &abi::fees::InternalMessageParams, descendants: Option, + max_depth: u32, + min_external_gas_limit: U256, mut quote: F, ) -> Result where @@ -318,9 +330,13 @@ where budget, allocations: vec![], }, - Some(abi::gl_call::Descendants::Pinned(nodes)) => { - validate_nodes(&nodes, enclosing_params.rotations.len(), &mut quote)? - } + Some(abi::gl_call::Descendants::Pinned(nodes)) => validate_nodes( + &nodes, + enclosing_params.rotations.len(), + max_depth, + min_external_gas_limit, + &mut quote, + )?, }; let subtree = encode_grant(&grant); diff --git a/executor/src/wasi/genlayer_sdk/message.rs b/executor/src/wasi/genlayer_sdk/message.rs index d77234d8..fb6d0ed6 100644 --- a/executor/src/wasi/genlayer_sdk/message.rs +++ b/executor/src/wasi/genlayer_sdk/message.rs @@ -139,11 +139,17 @@ fn prepare_grant( (None, Some(_)) => Err(generated::types::Errno::Inval.into()), (Some(params), descendants) => { // The transaction-pinned primary quote is not in host input; preserve the existing quote. - prepare_descendants(params, descendants, |node_params| { - data_fees_limit - .calculate_descendant_message_fee_internal(node_params) - .map(|cost| cost.reported_fee()) - }) + prepare_descendants( + params, + descendants, + data_fees_limit.max_allocation_tree_depth, + data_fees_limit.min_external_gas_limit, + |node_params| { + data_fees_limit + .calculate_message_fee_internal(node_params) + .map(|cost| cost.reported_fee()) + }, + ) .map_err(grant_validation_error) } } @@ -478,7 +484,6 @@ fn internal_price_caps_valid(params: &abi::fees::InternalMessageParams) -> bool } pub(super) fn internal_params_valid(params: &abi::fees::InternalMessageParams) -> bool { - // Transaction-pinned time-unit bounds and budget floor are not in host input yet. if params.rotations.is_empty() || !internal_price_caps_valid(params) { return false; } diff --git a/executor/src/wasi/genlayer_sdk/tests.rs b/executor/src/wasi/genlayer_sdk/tests.rs index e592dda0..5acdbd7b 100644 --- a/executor/src/wasi/genlayer_sdk/tests.rs +++ b/executor/src/wasi/genlayer_sdk/tests.rs @@ -110,7 +110,47 @@ fn external_allocation_node(budget: U256) -> gl_call::AllocationNode { fn prepare( descendants: Option, ) -> Result { - prepare_descendants(&grant_params(), descendants, |_| Ok(U256::from(10))) + prepare_descendants(&grant_params(), descendants, 5, U256::zero(), |_| { + Ok(U256::from(10)) + }) +} + +#[test] +fn descendant_grant_depth_matches_pinned_limit() { + for (length, limit, admitted) in [ + (5, 5, true), + (6, 5, false), + (3, 3, true), + (4, 3, false), + (1, 0, false), + ] { + let nodes = (0..length) + .map(|index| { + internal_allocation( + index as u8, + 10 * (length - index), + gl_call::On::Finalized, + if index == 0 { + ROOT_PARENT + } else { + U256::from(index - 1) + }, + ) + }) + .collect(); + let result = prepare_descendants( + &grant_params(), + Some(gl_call::Descendants::Pinned(nodes)), + limit, + U256::zero(), + |_| Ok(U256::from(10)), + ); + assert_eq!( + result.is_ok(), + admitted, + "length={length}, limit={limit}: {result:?}" + ); + } } #[test] @@ -214,7 +254,7 @@ fn descendant_grant_rejects_more_than_twenty_four_nodes() { } #[test] -fn descendant_grant_accepts_a_twenty_four_node_chain() { +fn descendant_grant_accepts_a_twenty_four_node_chain_when_host_allows_it() { let nodes = (0..24u8) .map(|index| { internal_allocation( @@ -229,7 +269,14 @@ fn descendant_grant_accepts_a_twenty_four_node_chain() { ) }) .collect(); - let prepared = prepare(Some(gl_call::Descendants::Pinned(nodes))).unwrap(); + let prepared = prepare_descendants( + &grant_params(), + Some(gl_call::Descendants::Pinned(nodes)), + 24, + U256::zero(), + |_| Ok(U256::from(10)), + ) + .unwrap(); assert_eq!(prepared.budget, U256::from(240)); } @@ -396,6 +443,34 @@ fn descendant_grant_rejects_invalid_external_budget() { } } +#[test] +fn descendant_grant_external_gas_limit_matches_pinned_floor() { + for (floor, admitted) in [ + (U256::zero(), true), + (U256::from(20), true), + (U256::from(21), false), + (U256::MAX, false), + ] { + let result = prepare_descendants( + &grant_params(), + Some(gl_call::Descendants::Pinned(vec![ + external_allocation_node(U256::from(200)), + ])), + 5, + floor, + |_| Ok(U256::from(10)), + ); + if admitted { + assert!(result.is_ok(), "floor={floor}: {result:?}"); + } else { + assert!( + matches!(result, Err(GrantValidationError::External)), + "floor={floor}: {result:?}" + ); + } + } +} + #[test] fn descendant_grant_rejects_insufficient_internal_budget() { let result = prepare(Some(gl_call::Descendants::Pinned(vec![ @@ -436,7 +511,9 @@ fn descendant_grant_decided_root_uses_enclosing_appeal_multiplier() { gl_call::On::Decided, ROOT_PARENT, )])); - let result = prepare_descendants(&enclosing, descendants, |_| Ok(U256::from(10))); + let result = prepare_descendants(&enclosing, descendants, 5, U256::zero(), |_| { + Ok(U256::from(10)) + }); assert!(matches!(result, Err(GrantValidationError::Budget))); } @@ -486,6 +563,8 @@ fn descendant_grant_rejects_budget_arithmetic_overflow() { gl_call::AllocationNode::Internal(parent), child, ])), + 5, + U256::zero(), |_| Ok(quotes.next().unwrap()), ); assert!(matches!( @@ -506,6 +585,8 @@ fn descendant_grant_rejects_budget_arithmetic_overflow() { Some(gl_call::Descendants::Pinned(vec![ gl_call::AllocationNode::Internal(decided), ])), + 5, + U256::zero(), |_| Ok(U256::MAX / U256::from(2) + U256::one()), ); assert!(matches!( diff --git a/executor/tests/message_fee_time_units.rs b/executor/tests/message_fee_time_units.rs index 7ea457d4..0daab80e 100644 --- a/executor/tests/message_fee_time_units.rs +++ b/executor/tests/message_fee_time_units.rs @@ -146,17 +146,54 @@ fn no_appeals_reserve_no_appeal_profit() { } #[test] -fn descendant_quote_skips_unavailable_pinned_budget_floor() { +fn message_quote_enforces_pinned_budget_floor() { let mut gas_data = gas_data(1, 100, 1, 100); gas_data.insert("messageBudgetFloor".to_owned(), "2".to_owned()); let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data).unwrap(); let params = fee_params(2, 1); assert!(fees.calculate_message_fee_internal(¶ms).is_err()); - assert_eq!( - fees.calculate_descendant_message_fee_internal(¶ms) - .unwrap() - .reported_fee(), - U256::from(8) - ); +} + +#[test] +fn descendant_depth_uses_host_input_or_default() { + for (input, expected) in [ + (None, 5), + (Some("3"), 3), + (Some("0"), 0), + (Some("4294967296"), u32::MAX), + ] { + let mut data = gas_data(1, 100, 1, 100); + if let Some(value) = input { + data.insert("maxAllocationTreeDepth".to_owned(), value.to_owned()); + } + let fees = DataLimit::new(bucket_totals(), default_fees(), data).unwrap(); + assert_eq!(fees.max_allocation_tree_depth, expected); + } +} + +#[test] +fn external_gas_floor_uses_host_input_or_zero() { + for (input, expected) in [ + (None, U256::zero()), + (Some("0"), U256::zero()), + (Some("21000"), U256::from(21_000)), + (Some(&*U256::MAX.to_string()), U256::MAX), + ] { + let mut data = gas_data(1, 100, 1, 100); + if let Some(value) = input { + data.insert("minExternalGasLimit".to_owned(), value.to_owned()); + } + let fees = DataLimit::new(bucket_totals(), default_fees(), data).unwrap(); + assert_eq!(fees.min_external_gas_limit, expected); + } +} + +#[test] +fn external_gas_floor_rejects_non_uint256_input() { + for input in ["abc", "-1", &(U256::MAX.to_string() + "0")] { + let mut data = gas_data(1, 100, 1, 100); + data.insert("minExternalGasLimit".to_owned(), input.to_owned()); + assert!(DataLimit::new(bucket_totals(), default_fees(), data).is_err()); + } } diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0.hash b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0.hash new file mode 100644 index 00000000..aef25853 --- /dev/null +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0.hash @@ -0,0 +1 @@ +2Nila069FzKPGAIjI4iRm2K/ay3rJD+1pKL3GTkO1KE= diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0.hash b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0.hash new file mode 100644 index 00000000..6697d806 --- /dev/null +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0.hash @@ -0,0 +1 @@ +qK6D01m66wEfrmiBDQMWLj0jbfOq9ELvSMoBYkUeu0A= diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_bad.hash b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_bad.hash new file mode 100644 index 00000000..1a56091f --- /dev/null +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_bad.hash @@ -0,0 +1 @@ +fQqquFzRygWNa9prLN/QACQiypzcfYUBm7Am4JFk+kg= diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.hash b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.hash new file mode 100644 index 00000000..c843e526 --- /dev/null +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.hash @@ -0,0 +1 @@ +IN9iKOAud1d5CxKI5GRa42q/LoETj1w+FH7XkkWylLg= diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.stdout b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.stdout index 080fdeb5..8891c2e1 100644 --- a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.stdout +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.stdout @@ -1,6 +1,6 @@ A got pong -A: LEAK ran to completion -A: LEAK survived the block A: compared first A: compared second +A: LEAK ran to completion +A: LEAK survived the block executed with `Return(null)` diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_sync.hash b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_sync.hash new file mode 100644 index 00000000..1a56091f --- /dev/null +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_sync.hash @@ -0,0 +1 @@ +fQqquFzRygWNa9prLN/QACQiypzcfYUBm7Am4JFk+kg= diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.jsonnet b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.jsonnet index af3e02a3..13a5fe02 100644 --- a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.jsonnet +++ b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.jsonnet @@ -1,7 +1,7 @@ local simple = import 'templates/two.jsonnet'; local util = import 'templates/util.jsonnet'; {tags: util.features([['exploit', 'leader-fault'], ['nondet', 'consensus', 'leader']], 'stable') + ['python'], - entry: util.addPaths([simple.run( + entry: util.addPaths(util.mapGraph(function(entry) entry + {allow_two_workers: false}, [simple.run( '${jsonnetDir}/leader_fault_uncatchable_from.py', '${jsonnetDir}/leader_fault_uncatchable_to.py', ||| @@ -59,4 +59,4 @@ local util = import 'templates/util.jsonnet'; }, ], }], - }])} + }]))} diff --git a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.skip b/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.skip deleted file mode 100644 index 40fd8b89..00000000 --- a/tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.skip +++ /dev/null @@ -1,5 +0,0 @@ -Validator comparison stages are drained by two queue consumers that overlap the -deterministic VM, and every VM flushes its buffer into one process-wide stdout. -The `A: compared ...` lines therefore arrive in any order and can splice inside -a line. Those prints are the only observable that a comparison stage ran, so the -case cannot be made deterministic without serializing the nondet queue. diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.hash b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.hash new file mode 100644 index 00000000..0126d062 --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.hash @@ -0,0 +1 @@ +wKyumvQsLVjanIRIwjvLhl2jxkHaLJqX7ivPkbkELEY= diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.stdout b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.stdout new file mode 100644 index 00000000..7d73027c --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.stdout @@ -0,0 +1 @@ +executed with `Return(null)` diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.hash b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.hash new file mode 100644 index 00000000..ae514139 --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.hash @@ -0,0 +1 @@ +5nMefmkNp/gTzk2VAHkVPzAGkzwLXC+F1mej3QCQl/s= diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.stdout b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.stdout new file mode 100644 index 00000000..ed5fbd8d --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.stdout @@ -0,0 +1,4 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":300000,"on":"finalized","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":false,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[2]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":2,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":53476,"on":"finalized","receipt_fee":1057,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[3]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1,"receipt_fee_max_gas_price":1606938044258990275541962092341162602522202993782792835301376,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":1606938044258990275541962092341162602522202993782792835301376,"validator_timeunits_allocation":5},"message_fee":300000,"on":"decided","receipt_fee":1057,"subtree":b#,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet new file mode 100644 index 00000000..d54d83a5 --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet @@ -0,0 +1,24 @@ +local simple = import 'templates/simple_deploy.jsonnet'; +local msg = import 'templates/message.json'; +local util = import 'templates/util.jsonnet'; +local s = simple.run('${jsonnetDir}/${fileBaseName}.py'); +// Default open-pool wildcards plus the cascade a node sends for an open bucket +local extra = { + 'balances': { + 'AQAAAAAAAAAAAAAAAAAAAAAAAAA=': 1000000, + }, + 'message_fee_cascade': { + 'min_primary_fee': 100000, + 'budget_remaining': 1000000, + 'external_fee_reserve': 0, + }, +}; +{tags: util.features([['message', 'send'], ['balance'], ['fees', 'balance']], 'stable') + ['python'], + entry: util.addPaths([util.chain([ + s + extra, + s + extra + { + 'code': null, + 'message': msg, + 'calldata': std.manifestJsonEx({'': 'emit', 'args': []}, ' '), + }, + ])])} diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.py b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.py new file mode 100644 index 00000000..67c9f8b1 --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.py @@ -0,0 +1,29 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl +from genlayer.vm.public_abi import Permissions + +_PARAMS = gl.chain.InternalMessageParams( + leader_time_units_allocation=5, + validator_time_units_allocation=5, + execution_budget_per_round=1024, + rotations=[4, 4, 4, 4, 4], + max_price_gen_per_time_unit=2, + storage_fee_max_gas_price=20, + receipt_fee_max_gas_price=20, +) + + +class Contract(gl.contract.Contract): + def __init__(self): + gl.storage.Root.get().set_permission( + Permissions.CAN_USE_BALANCE_FOR_MESSAGE_FEES, True + ) + + @gl.public.write + def emit(self): + target = gl.contract.get_at(gl.Address(b'\x30' * 20)) + # Both sender-pool messages are floored, then share half of the spare + # budget; the balance-funded one keeps its metered fee + target.emit().foo(1) + target.emit(use_balance=gl.contract.UseBalanceParams(_PARAMS)).foo(2) + target.emit(on='decided').foo(3) diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_five.stdout b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_five.stdout new file mode 100644 index 00000000..90a1abaf --- /dev/null +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_five.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo"},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":2,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":553476,"on":"finalized","receipt_fee":5185,"subtree":b#5f77181a28fb88dd0597390f852a7f174bad95303defeefb5ba806aaa3ef64fb000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000007a1200000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000380000000000000000000000000000000000000000000000000000000000000066000000000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000000000000000000000000000c2000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000005050505050505050505050505050505050505050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007a12000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040404040404040404040404040404040404040400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000061a8000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000303030303030303030303030303030303030303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000020202020202020202020202020202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030d4000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_six.stdout b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_six.stdout new file mode 100644 index 00000000..29570012 --- /dev/null +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_six.stdout @@ -0,0 +1 @@ +executed with `VMError("fee descendant_grant tree")` diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_at_floor.stdout b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_at_floor.stdout new file mode 100644 index 00000000..684b875e --- /dev/null +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_at_floor.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo"},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":2,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":53676,"on":"finalized","receipt_fee":1697,"subtree":b#5f77181a28fb88dd0597390f852a7f174bad95303defeefb5ba806aaa3ef64fb00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000003333333333333333333333333333333333333333a9059cbb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000a,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_below_floor.stdout b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_below_floor.stdout new file mode 100644 index 00000000..4b9bb263 --- /dev/null +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.0_external_below_floor.stdout @@ -0,0 +1 @@ +executed with `VMError("fee descendant_grant external")` diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet b/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet index 2011f374..d33a62df 100644 --- a/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet @@ -6,18 +6,45 @@ local balance = { 'AQAAAAAAAAAAAAAAAAAAAAAAAAA=': 10000000, }, }; +// gas_data replaces DEFAULT_GAS_DATA wholesale +local gasData = { + storageUnitPrice: '1', + receiptGasPerByte: '1', + gasPerChangedSlot: '1', + intrinsicGas: '0', + bootloaderOverhead: '0', + fixedProposeReceiptGas: '0', + fixedMessageRevealGas: '0', + lockedReceiptGasPrice: '1', + overlaySplitBps: '0', + receiptWrapperBytes: '1024', + minProposeTimeout: '1', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '1', + maxCommitTimeout: '340282366920938463463374607431768211455', + genPerTimeUnit: '0', + messageBudgetFloor: '0', +}; local base = deployThen.run('${jsonnetDir}/${fileBaseName}.py', 'open'); local write(method) = base.next[0] + balance + { slug: method, calldata: std.manifestJsonEx({'': method}, ' '), }; +local externalFloor(slug, floor) = write('external_floor') + { + slug: slug, + gas_data: gasData + {minExternalGasLimit: floor}, +}; local case = base + balance + { next: [ write('open'), write('closed_zero'), write('pinned_nested'), + write('depth_five'), + write('depth_six'), write('invalid_tree'), write('external_budget'), + externalFloor('external_at_floor', '20'), + externalFloor('external_below_floor', '21'), write('deploy_open'), write('nested_decided_underfunded'), write('duplicate_external_selector'), diff --git a/tests/integration/message/use_balance_descendants/use_balance_descendants.py b/tests/integration/message/use_balance_descendants/use_balance_descendants.py index abd326c1..96d9df87 100644 --- a/tests/integration/message/use_balance_descendants/use_balance_descendants.py +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.py @@ -34,6 +34,30 @@ def closed_zero(self): def pinned_nested(self): self._emit_pinned(None) + @gl.public.write + def depth_five(self): + self._emit_depth(5) + + @gl.public.write + def depth_six(self): + self._emit_depth(6) + + def _emit_depth(self, depth: int): + children = [] + for index in range(depth): + children = [ + gl.message_allocation.InternalAllocation( + gl.Address(bytes([index + 1]) * 20), + bytes(32), + _PARAMS, + 100_000 * (index + 1), + children=children, + ) + ] + _TARGET.emit( + use_balance=gl.contract.UseBalanceParams(_PARAMS, descendants=children) + ).foo() + @gl.public.write def pinned_selector(self): self._emit_pinned(bytes.fromhex('a9059cbb')) @@ -82,11 +106,18 @@ def invalid_tree(self): @gl.public.write def external_budget(self): + self._emit_external(199) + + @gl.public.write + def external_floor(self): + self._emit_external(200) + + def _emit_external(self, budget: int): external = gl.message_allocation.ExternalAllocation( gl.Address(b'\x33' * 20), bytes.fromhex('a9059cbb'), gl.chain.ExternalMessageParams(20, 10), - 199, + budget, ) _TARGET.emit( use_balance=gl.contract.UseBalanceParams(_PARAMS, descendants=[external]),