From de10d404a8898bffdbf57a632ba468373cfbc243 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Tue, 13 Oct 2026 08:41:12 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(fees):=20carve=20open-bucket=20child?= =?UTF-8?q?=20budgets=20from=20the=20host=20cascade=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- executor/src/lib.rs | 7 +- executor/src/rt/cascade.rs | 54 +++++++++ executor/src/rt/cascade_test.rs | 110 ++++++++++++++++++ executor/src/rt/mod.rs | 1 + .../open_bucket_cascade.0.hash | 1 + .../open_bucket_cascade.0.stdout | 1 + .../open_bucket_cascade.0_0.hash | 1 + .../open_bucket_cascade.0_0.stdout | 4 + .../open_bucket_cascade.jsonnet | 23 ++++ .../open_bucket_cascade.py | 29 +++++ 10 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 executor/src/rt/cascade.rs create mode 100644 executor/src/rt/cascade_test.rs create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.hash create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.0.stdout create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.hash create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.0_0.stdout create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet create mode 100644 tests/integration/message/open_bucket_cascade/open_bucket_cascade.py 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..236ae965 --- /dev/null +++ b/executor/src/rt/cascade.rs @@ -0,0 +1,54 @@ +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 of `budget_total - Σ fees`; 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 = U256::zero(); + 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_total { + return; + } + + let per_child = (cascade.budget_total - 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_total - 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..5e0abdce --- /dev/null +++ b/executor/src/rt/cascade_test.rs @@ -0,0 +1,110 @@ +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_total: u64, +) -> genvm_modules_interfaces::fees::MessageFeeCascade { + genvm_modules_interfaces::fees::MessageFeeCascade { + min_primary_fee: U256::from(min_primary_fee), + budget_total: U256::from(budget_total), + } +} + +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 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_total = 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/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/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..761c3aeb --- /dev/null +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet @@ -0,0 +1,23 @@ +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_total': 1000000, + }, +}; +{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) From 38e27243391b0cf35ce98ffa1d23bb2c40f6a66e Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 18:25:29 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(fees):=20respect=20remaining=20funds=20?= =?UTF-8?q?and=20pinned=20grant=20limits=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- executor/codegen/data/internal-constants.json | 1 + .../crates/common/src/internal_constants.rs | 1 + executor/install/config/genvm.yaml | 3 +- executor/src/rt/cascade.rs | 18 ++++--- executor/src/rt/cascade_test.rs | 35 ++++++++++-- executor/src/rt/fees.rs | 32 +++++------ .../src/wasi/genlayer_sdk/descendant_grant.rs | 17 ++++-- executor/src/wasi/genlayer_sdk/message.rs | 15 ++++-- executor/src/wasi/genlayer_sdk/tests.rs | 53 +++++++++++++++++-- executor/tests/message_fee_time_units.rs | 28 +++++++--- .../open_bucket_cascade.jsonnet | 3 +- ...se_balance_descendants.0_depth_five.stdout | 2 + ...use_balance_descendants.0_depth_six.stdout | 1 + .../use_balance_descendants.jsonnet | 2 + .../use_balance_descendants.py | 24 +++++++++ 15 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_five.stdout create mode 100644 tests/integration/message/use_balance_descendants/use_balance_descendants.0_depth_six.stdout 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/rt/cascade.rs b/executor/src/rt/cascade.rs index 236ae965..3ff59557 100644 --- a/executor/src/rt/cascade.rs +++ b/executor/src/rt/cascade.rs @@ -19,13 +19,19 @@ fn pool_funded_fee(emission: &mut ExecutionEmission) -> Option<&mut U256> { } /// Raises each sender-pool internal message to the floor, then adds an equal -/// share of half of `budget_total - Σ fees`; the other half stays with the -/// parent so one deep path cannot drain the tree +/// 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 = U256::zero(); + 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) { @@ -34,16 +40,16 @@ pub fn apply( count += 1; } - if count == 0 || sum >= cascade.budget_total { + if count == 0 || sum >= cascade.budget_remaining { return; } - let per_child = (cascade.budget_total - sum) / (U256::from(2) * U256::from(count)); + 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_total - sum` + // 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; } diff --git a/executor/src/rt/cascade_test.rs b/executor/src/rt/cascade_test.rs index 5e0abdce..9e71b9b1 100644 --- a/executor/src/rt/cascade_test.rs +++ b/executor/src/rt/cascade_test.rs @@ -44,11 +44,12 @@ fn external(message_fee: u64) -> ExecutionEmission { fn cascade( min_primary_fee: u64, - budget_total: u64, + budget_remaining: u64, ) -> genvm_modules_interfaces::fees::MessageFeeCascade { genvm_modules_interfaces::fees::MessageFeeCascade { min_primary_fee: U256::from(min_primary_fee), - budget_total: U256::from(budget_total), + budget_remaining: U256::from(budget_remaining), + external_fee_reserve: U256::zero(), } } @@ -64,6 +65,34 @@ fn fees(emissions: &[ExecutionEmission]) -> Vec { .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)]; @@ -99,7 +128,7 @@ 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_total = U256::MAX; + big.budget_remaining = U256::MAX; apply(&mut emissions, &big); for e in &emissions { let ExecutionEmission::InternalMessage { message_fee, .. } = e else { diff --git a/executor/src/rt/fees.rs b/executor/src/rt/fees.rs index 90cb6a6b..400c8368 100644 --- a/executor/src/rt/fees.rs +++ b/executor/src/rt/fees.rs @@ -264,6 +264,7 @@ pub struct MessageReceiptParams { #[derive(Debug)] pub struct DataLimit { + pub max_allocation_tree_depth: u32, buckets: tokio::sync::Mutex>, storage: Bucket, message_receipt: Bucket, @@ -278,6 +279,13 @@ 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 prelude = &fees.expr_prelude; // Host-provided values are exposed under a single `node` object, e.g. @@ -343,6 +351,7 @@ impl DataLimit { } Ok(Self { + max_allocation_tree_depth, buckets: tokio::sync::Mutex::new(bucket_totals), storage, message_receipt, @@ -606,27 +615,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), @@ -636,6 +629,13 @@ impl DataLimit { .ctx("calculating message fee internal") } + pub fn calculate_descendant_message_fee_internal( + &self, + matched_fee_params: &genlayer_sdk::abi::fees::InternalMessageParams, + ) -> rt::errors::Result { + self.calculate_message_fee_internal(matched_fee_params) + } + pub async fn consume_message_fee(&self, cost_fee: &CostVec, cost_receipt: &CostVec) -> bool { let mut buckets = self.buckets.lock().await; diff --git a/executor/src/wasi/genlayer_sdk/descendant_grant.rs b/executor/src/wasi/genlayer_sdk/descendant_grant.rs index 85796011..4ecdf108 100644 --- a/executor/src/wasi/genlayer_sdk/descendant_grant.rs +++ b/executor/src/wasi/genlayer_sdk/descendant_grant.rs @@ -173,6 +173,7 @@ 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, quote: &mut F, ) -> Result where @@ -183,6 +184,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 +198,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 +210,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)?; @@ -289,6 +296,7 @@ where pub(super) fn prepare_descendants( enclosing_params: &abi::fees::InternalMessageParams, descendants: Option, + max_depth: u32, mut quote: F, ) -> Result where @@ -318,9 +326,12 @@ 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, + &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..3f776c69 100644 --- a/executor/src/wasi/genlayer_sdk/message.rs +++ b/executor/src/wasi/genlayer_sdk/message.rs @@ -139,11 +139,16 @@ 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, + |node_params| { + data_fees_limit + .calculate_descendant_message_fee_internal(node_params) + .map(|cost| cost.reported_fee()) + }, + ) .map_err(grant_validation_error) } } diff --git a/executor/src/wasi/genlayer_sdk/tests.rs b/executor/src/wasi/genlayer_sdk/tests.rs index e592dda0..088cedbb 100644 --- a/executor/src/wasi/genlayer_sdk/tests.rs +++ b/executor/src/wasi/genlayer_sdk/tests.rs @@ -110,7 +110,44 @@ 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, |_| 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, + |_| Ok(U256::from(10)), + ); + assert_eq!( + result.is_ok(), + admitted, + "length={length}, limit={limit}: {result:?}" + ); + } } #[test] @@ -214,7 +251,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 +266,13 @@ 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, + |_| Ok(U256::from(10)), + ) + .unwrap(); assert_eq!(prepared.budget, U256::from(240)); } @@ -436,7 +479,7 @@ 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, |_| Ok(U256::from(10))); assert!(matches!(result, Err(GrantValidationError::Budget))); } @@ -486,6 +529,7 @@ fn descendant_grant_rejects_budget_arithmetic_overflow() { gl_call::AllocationNode::Internal(parent), child, ])), + 5, |_| Ok(quotes.next().unwrap()), ); assert!(matches!( @@ -506,6 +550,7 @@ fn descendant_grant_rejects_budget_arithmetic_overflow() { Some(gl_call::Descendants::Pinned(vec![ gl_call::AllocationNode::Internal(decided), ])), + 5, |_| 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..b50b0f8e 100644 --- a/executor/tests/message_fee_time_units.rs +++ b/executor/tests/message_fee_time_units.rs @@ -146,17 +146,31 @@ fn no_appeals_reserve_no_appeal_profit() { } #[test] -fn descendant_quote_skips_unavailable_pinned_budget_floor() { +fn descendant_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) - ); + assert!(fees + .calculate_descendant_message_fee_internal(¶ms) + .is_err()); +} + +#[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); + } } diff --git a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet index 761c3aeb..d54d83a5 100644 --- a/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet +++ b/tests/integration/message/open_bucket_cascade/open_bucket_cascade.jsonnet @@ -9,7 +9,8 @@ local extra = { }, 'message_fee_cascade': { 'min_primary_fee': 100000, - 'budget_total': 1000000, + 'budget_remaining': 1000000, + 'external_fee_reserve': 0, }, }; {tags: util.features([['message', 'send'], ['balance'], ['fees', 'balance']], 'stable') + ['python'], 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.jsonnet b/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet index 2011f374..2aaf9951 100644 --- a/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet +++ b/tests/integration/message/use_balance_descendants/use_balance_descendants.jsonnet @@ -16,6 +16,8 @@ local case = base + balance + { write('open'), write('closed_zero'), write('pinned_nested'), + write('depth_five'), + write('depth_six'), write('invalid_tree'), write('external_budget'), write('deploy_open'), 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..52075575 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')) From d077a831b43d6c327b6a6c056388e7585a2d3198 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 18:28:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(tests):=20run=20leader=20fault=20checks?= =?UTF-8?q?=20synchronously=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../leader_fault_uncatchable/leader_fault_uncatchable.0.hash | 1 + .../leader_fault_uncatchable.0_0.hash | 1 + .../leader_fault_uncatchable.0_0_bad.hash | 1 + .../leader_fault_uncatchable.0_0_ok.hash | 1 + .../leader_fault_uncatchable.0_0_ok.stdout | 4 ++-- .../leader_fault_uncatchable.0_0_sync.hash | 1 + .../leader_fault_uncatchable.jsonnet | 4 ++-- .../leader_fault_uncatchable/leader_fault_uncatchable.skip | 5 ----- 8 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0.hash create mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0.hash create mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_bad.hash create mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_ok.hash create mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.0_0_sync.hash delete mode 100644 tests/integration/exploit/leader_fault_uncatchable/leader_fault_uncatchable.skip 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.