From f7f95a31dddbef2aa9371e2228644f83dcce9fe3 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 1 Oct 2026 00:19:11 +0900 Subject: [PATCH] =?UTF-8?q?fix(executor):=20treat=20zero-budget=20chain=20?= =?UTF-8?q?allocations=20as=20absent=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consensus resolves only allocation keys with a nonzero stored budget, so a zero-budget exact key now falls through to the per-recipient `call_key` wildcard, and a zero-budget wildcard to no allocation, instead of trapping on its budget - Synthetic recipient wildcards keep matching regardless of budget; keys exhausted by the execution's own emissions still never spill - Cover both internal emission paths end to end, the external unallocated fallback, phase interaction and the synthetic wildcard --- executor/src/wasi/genlayer_sdk/message.rs | 5 + executor/src/wasi/genlayer_sdk/tests.rs | 230 +++++++++++++++++----- 2 files changed, 186 insertions(+), 49 deletions(-) diff --git a/executor/src/wasi/genlayer_sdk/message.rs b/executor/src/wasi/genlayer_sdk/message.rs index d6cc716a..497110aa 100644 --- a/executor/src/wasi/genlayer_sdk/message.rs +++ b/executor/src/wasi/genlayer_sdk/message.rs @@ -5,6 +5,11 @@ fn allocation_match_priority( recipient: calldata::Address, call_key: genvm_modules_interfaces::abi_stub::CallKey, ) -> Option { + // Consensus resolves only keys with a nonzero budget + if node.recipient.is_some() && node.budget.is_some_and(|budget| budget.is_zero()) { + return None; + } + let recipient_priority = match node.recipient { Some(candidate) if candidate == recipient => 0, Some(_) => return None, diff --git a/executor/src/wasi/genlayer_sdk/tests.rs b/executor/src/wasi/genlayer_sdk/tests.rs index 9ef1be54..82b5aae3 100644 --- a/executor/src/wasi/genlayer_sdk/tests.rs +++ b/executor/src/wasi/genlayer_sdk/tests.rs @@ -134,7 +134,7 @@ fn internal_allocation_prefers_exact_key_over_earlier_wildcard() { } #[test] -fn internal_allocation_keeps_exhausted_exact_key() { +fn internal_allocation_skips_zero_budget_chain_keys() { let recipient = calldata::Address::from([7; 20]); let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); let mut wildcard = internal_message_allocation(); @@ -143,6 +143,37 @@ fn internal_allocation_keeps_exhausted_exact_key() { let mut exact = wildcard.clone(); exact.call_key = Some(call_key); exact.budget = Some(U256::zero()); + let mut nodes = vec![wildcard, exact]; + + let (matched, _) = resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .expect("wildcard allocation should match after skipping zero-budget exact"); + assert_eq!(matched, 0); + + nodes[0].budget = Some(U256::zero()); + assert!(resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .is_none()); +} + +#[test] +fn internal_allocation_zero_budget_exact_ignores_its_phase() { + let recipient = calldata::Address::from([7; 20]); + let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); + let mut wildcard = internal_message_allocation(); + wildcard.recipient = Some(recipient); + let mut exact = wildcard.clone(); + exact.call_key = Some(call_key); + exact.on = genvm_modules_interfaces::On::Decided; + exact.budget = Some(U256::zero()); let nodes = vec![wildcard, exact]; let (matched, _) = resolve_internal_allocation( @@ -151,10 +182,24 @@ fn internal_allocation_keeps_exhausted_exact_key() { recipient, call_key, ) - .expect("exhausted exact allocation should still match"); + .expect("zero-budget exact allocation should not block the wildcard"); + assert_eq!(matched, 0); +} + +#[test] +fn internal_allocation_keeps_zero_budget_synthetic_wildcard() { + let recipient = calldata::Address::from([7; 20]); + let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); + let mut synthetic = internal_message_allocation(); + synthetic.budget = Some(U256::zero()); - assert_eq!(matched, 1); - assert_eq!(nodes[matched].budget, Some(U256::zero())); + assert!(resolve_internal_allocation( + &[synthetic], + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .is_some()); } #[test] @@ -267,15 +312,25 @@ fn external_allocation_candidates_follow_consensus_precedence() { } #[test] -fn external_allocation_candidates_include_zero_budget_nodes() { +fn external_allocation_candidates_skip_zero_budget_chain_keys() { let recipient = calldata::Address::from([7; 20]); let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); - let mut node = external_message_allocation(); - node.recipient = Some(recipient); - node.call_key = Some(call_key); - node.budget = Some(U256::zero()); - let nodes = vec![node]; + let mut synthetic = external_message_allocation(); + synthetic.budget = Some(U256::zero()); + let mut wildcard = external_message_allocation(); + wildcard.recipient = Some(recipient); + wildcard.budget = Some(U256::one()); + let mut exact = wildcard.clone(); + exact.call_key = Some(call_key); + exact.budget = Some(U256::zero()); + let mut nodes = vec![synthetic, wildcard, exact]; + assert_eq!( + external_allocation_candidates(&nodes, recipient, call_key), + vec![1, 0] + ); + + nodes[1].budget = Some(U256::zero()); assert_eq!( external_allocation_candidates(&nodes, recipient, call_key), vec![0] @@ -621,49 +676,89 @@ fn trap_message(error: generated::types::Error) -> String { } #[tokio::test] -async fn internal_exhaustion_never_spills_to_wildcard() { +async fn internal_zero_budget_chain_keys_are_absent() { for emission in [ MessageEmission::InternalAllocation, MessageEmission::DeployAllocation, ] { - for budget in [U256::zero(), U256::one()] { - let mut test = EmissionTestContext::new(u32::MAX, 100); - let mut exact = internal_message_allocation(); - exact.recipient = Some(calldata::Address::zero()); - exact.call_key = Some(genvm_modules_interfaces::CallKey(match emission { - MessageEmission::InternalAllocation => abi::CallKey::UNNAMED.0, - _ => abi::CallKey::DEPLOY.0, - })); - exact.budget = Some(budget); - test.context - .data - .accumulator - .message_fee_allocation - .push(exact); + let mut test = EmissionTestContext::new(u32::MAX, 100); + let mut wildcard = internal_message_allocation(); + wildcard.recipient = Some(calldata::Address::zero()); + wildcard.subtree = bytes::Bytes::from_static(b"wildcard subtree"); + let mut exact = wildcard.clone(); + exact.call_key = Some(genvm_modules_interfaces::CallKey(match emission { + MessageEmission::InternalAllocation => abi::CallKey::UNNAMED.0, + _ => abi::CallKey::DEPLOY.0, + })); + exact.budget = Some(U256::zero()); + exact.subtree = bytes::Bytes::from_static(b"exact subtree"); + let accumulator = &mut test.context.data.accumulator; + accumulator.message_fee_allocation = vec![exact, wildcard]; + accumulator.message_fee_allocation_consumed = vec![U256::zero(); 2]; + + test.emit_message(emission).await.unwrap(); + let accumulator = &test.context.data.accumulator; + assert_eq!(accumulator.message_fee_allocation_consumed[0], U256::zero()); + assert!(!accumulator.message_fee_allocation_consumed[1].is_zero()); + match &accumulator.emissions[0] { + domain::ExecutionEmission::InternalMessage { subtree, .. } + | domain::ExecutionEmission::InternalDeployMessage { subtree, .. } => { + assert_eq!(subtree.as_ref(), b"wildcard subtree") + } + other => panic!("unexpected emission: {other:?}"), + } + + test.context.data.accumulator.message_fee_allocation[1].budget = Some(U256::zero()); + let error = trap_message(test.emit_message(emission).await.unwrap_err()); + assert!( + error.contains("fee no_matching_allocation # internal"), + "{error}" + ); + assert_eq!(test.context.data.accumulator.emissions.len(), 1); + test.shutdown().await; + } +} + +#[tokio::test] +async fn internal_local_exhaustion_never_spills_to_wildcard() { + for emission in [ + MessageEmission::InternalAllocation, + MessageEmission::DeployAllocation, + ] { + let mut test = EmissionTestContext::new(u32::MAX, 100); + let mut exact = internal_message_allocation(); + exact.recipient = Some(calldata::Address::zero()); + exact.call_key = Some(genvm_modules_interfaces::CallKey(match emission { + MessageEmission::InternalAllocation => abi::CallKey::UNNAMED.0, + _ => abi::CallKey::DEPLOY.0, + })); + exact.budget = Some(U256::one()); + test.context + .data + .accumulator + .message_fee_allocation + .push(exact); + test.context + .data + .accumulator + .message_fee_allocation_consumed + .push(U256::zero()); + test.emit_message(emission).await.unwrap(); + let count = test.context.data.accumulator.emissions.len(); + let error = trap_message(test.emit_message(emission).await.unwrap_err()); + assert!( + error.contains("out_of message_fee allocation_budget # internal"), + "{error}" + ); + assert_eq!(test.context.data.accumulator.emissions.len(), count); + assert_eq!( test.context .data .accumulator - .message_fee_allocation_consumed - .push(U256::zero()); - if !budget.is_zero() { - test.emit_message(emission).await.unwrap(); - } - let count = test.context.data.accumulator.emissions.len(); - let error = trap_message(test.emit_message(emission).await.unwrap_err()); - assert!( - error.contains("out_of message_fee allocation_budget # internal"), - "{error}" - ); - assert_eq!(test.context.data.accumulator.emissions.len(), count); - assert_eq!( - test.context - .data - .accumulator - .message_fee_allocation_consumed[1], - U256::zero() - ); - test.shutdown().await; - } + .message_fee_allocation_consumed[1], + U256::zero() + ); + test.shutdown().await; } } @@ -693,7 +788,7 @@ async fn external_exhaustion_spills_but_absence_is_unallocated() { let mut exact = external_message_allocation(); exact.recipient = Some(calldata::Address::zero()); exact.call_key = Some(genvm_modules_interfaces::CallKey([0; 32])); - exact.budget = Some(U256::zero()); + exact.budget = Some(U256::one()); test.context .data .accumulator @@ -705,6 +800,7 @@ async fn external_exhaustion_spills_but_absence_is_unallocated() { .message_fee_allocation_consumed .push(U256::zero()); test.emit_message(MessageEmission::External).await.unwrap(); + test.emit_message(MessageEmission::External).await.unwrap(); assert_eq!( test.context .data @@ -717,7 +813,7 @@ async fn external_exhaustion_spills_but_absence_is_unallocated() { .data .accumulator .message_fee_allocation_consumed[2], - U256::zero() + U256::one() ); test.context.data.accumulator.message_fee_allocation[0].budget = Some(U256::one()); let error = trap_message( @@ -736,7 +832,43 @@ async fn external_exhaustion_spills_but_absence_is_unallocated() { .message_fee_allocation_consumed .clear(); test.emit_message(MessageEmission::External).await.unwrap(); - assert_eq!(test.context.data.accumulator.emissions.len(), 2); + assert_eq!(test.context.data.accumulator.emissions.len(), 3); + test.shutdown().await; +} + +#[tokio::test] +async fn external_zero_budget_chain_keys_are_unallocated() { + let mut test = EmissionTestContext::new(u32::MAX, 100); + let mut wildcard = external_message_allocation(); + wildcard.recipient = Some(calldata::Address::zero()); + wildcard.budget = Some(U256::zero()); + let mut exact = wildcard.clone(); + exact.call_key = Some(genvm_modules_interfaces::CallKey([0; 32])); + let accumulator = &mut test.context.data.accumulator; + accumulator.message_fee_allocation = vec![exact, wildcard]; + accumulator.message_fee_allocation_consumed = vec![U256::zero(); 2]; + + test.emit_message(MessageEmission::External).await.unwrap(); + assert_eq!( + test.context + .data + .accumulator + .message_fee_allocation_consumed, + vec![U256::zero(); 2] + ); + + let accumulator = &mut test.context.data.accumulator; + accumulator.message_fee_allocation[1].budget = Some(U256::one()); + accumulator.message_fee_allocation_consumed[1] = U256::one(); + let error = trap_message( + test.emit_message(MessageEmission::External) + .await + .unwrap_err(), + ); + assert!( + error.contains("out_of message_fee allocation_budget # external"), + "{error}" + ); test.shutdown().await; }