diff --git a/executor/src/lib.rs b/executor/src/lib.rs index 419e4ac4..104a9f93 100644 --- a/executor/src/lib.rs +++ b/executor/src/lib.rs @@ -393,6 +393,7 @@ pub async fn run_with_impl( topmost_runner_id, }, }; + let message_fee_cascade = entry_data.message_fee_cascade; let message_fee_allocation = entry_data .message_fee_allocation .into_iter() @@ -434,7 +435,11 @@ pub async fn run_with_impl( det_subvm_hashes: Default::default(), }); - 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); + } Ok(rt::vm::FullResult { kind: match &run_result.run_ok { diff --git a/executor/src/rt/cascade.rs b/executor/src/rt/cascade.rs new file mode 100644 index 00000000..cae3ad78 --- /dev/null +++ b/executor/src/rt/cascade.rs @@ -0,0 +1,46 @@ +use primitive_types::U256; + +use crate::domain::ExecutionEmission; + +fn internal_fee(emission: &mut ExecutionEmission) -> Option<&mut U256> { + match emission { + ExecutionEmission::PostMessage { message_fee, .. } + | ExecutionEmission::DeployContract { message_fee, .. } => Some(message_fee), + _ => None, + } +} + +/// Raises each 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(internal_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(internal_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..9e5492f7 --- /dev/null +++ b/executor/src/rt/cascade_test.rs @@ -0,0 +1,85 @@ +use super::*; +use crate::domain::fees; + +fn params() -> fees::InternalMessageParams { + fees::InternalMessageParams { + leader_timeunits_allocation: U256::one(), + validator_timeunits_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(), + } +} + +fn post(message_fee: u64) -> ExecutionEmission { + ExecutionEmission::PostMessage { + call_key: genlayer_sdk::abi::CallKey([0; 32]), + address: genlayer_sdk::calldata::Address::zero(), + calldata: genlayer_calldata::codec::Maybe::Materialized( + genlayer_sdk::calldata::Value::Null, + ), + value: U256::zero(), + on: genlayer_sdk::abi::gl_call::On::Finalized, + message_fee: U256::from(message_fee), + receipt_fee: U256::zero(), + fee_params: params(), + subtree: bytes::Bytes::new(), + } +} + +fn deploy(message_fee: u64) -> ExecutionEmission { + ExecutionEmission::DeployContract { + calldata: genlayer_calldata::codec::Maybe::Materialized( + genlayer_sdk::calldata::Value::Null, + ), + code: bytes::Bytes::new(), + value: U256::zero(), + on: genlayer_sdk::abi::gl_call::On::Finalized, + salt_nonce: U256::zero(), + message_fee: U256::from(message_fee), + receipt_fee: U256::zero(), + fee_params: params(), + subtree: bytes::Bytes::new(), + } +} + +fn fees_of(emissions: &[ExecutionEmission]) -> Vec { + emissions + .iter() + .map(|e| match e { + ExecutionEmission::PostMessage { message_fee, .. } + | ExecutionEmission::DeployContract { message_fee, .. } + | ExecutionEmission::EthSend { message_fee, .. } => message_fee.low_u64(), + ExecutionEmission::EmitEvent { .. } => 0, + }) + .collect() +} + +#[test] +fn floors_then_splits_half_of_spare_across_posts_and_deploys() { + let mut emissions = vec![post(10), deploy(40)]; + apply( + &mut emissions, + &genvm_modules_interfaces::fees::MessageFeeCascade { + min_primary_fee: U256::from(30), + budget_total: U256::from(1_000), + }, + ); + // floors 30 + 40 = 70, spare 930, per child 930 / 4 = 232 + assert_eq!(fees_of(&emissions), vec![262, 272]); +} + +#[test] +fn no_spare_only_floors() { + let mut emissions = vec![post(10), post(10)]; + apply( + &mut emissions, + &genvm_modules_interfaces::fees::MessageFeeCascade { + min_primary_fee: U256::from(30), + budget_total: U256::from(60), + }, + ); + assert_eq!(fees_of(&emissions), vec![30, 30]); +} diff --git a/executor/src/rt/mod.rs b/executor/src/rt/mod.rs index 7b8f1db1..fb8aae66 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;