From c698f5551d52852b32b4975f437322f171ce4efe Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Wed, 9 Sep 2026 15:53:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(executor):=20adopt=20consensus-safe=20name?= =?UTF-8?q?d=20fee=20accounting=20=E2=9C=A8=F0=9F=94=92=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carry opaque leader outputs and complete allocation subtrees so validators can apply versioned decoding without trusting proposed fee accounting. * fix(fees): reject an overlay split at or above the full share 🐛🔒️ * chore(fees): cover the external message fee price selection ✅ --- .../src/python-sdk/migration-guide.rst | 6 +- executor/crates/common/tests/fees_abi.rs | 63 +-- executor/fuzz/genvm-storage.rs | 12 +- executor/install/config/genvm.yaml | 188 +++++---- executor/src/config.rs | 37 +- executor/src/exe/run.rs | 66 +-- executor/src/host/mod.rs | 10 +- executor/src/leader_public_data.rs | 211 +++++----- executor/src/lib.rs | 6 +- executor/src/rt/fees.rs | 198 ++++++--- executor/src/wasi/genlayer_sdk/message.rs | 376 +++++++++++++----- executor/src/wasi/genlayer_sdk/mod.rs | 1 + executor/src/wasi/genlayer_sdk/run.rs | 37 +- executor/src/wasi/genlayer_sdk/tests.rs | 372 ++++++++++++++++- executor/tests/code_and_major_reads.rs | 7 +- executor/tests/fee_bucket_accounting.rs | 67 ++++ executor/tests/fee_bucket_config.rs | 71 ++++ executor/tests/message_fee_external.rs | 76 ++++ executor/tests/message_fee_overlay.rs | 95 +++++ executor/tests/message_fee_time_units.rs | 116 ++++++ executor/tests/message_receipt_fees.rs | 91 +++++ executor/tests/nondet_output_fees.rs | 14 +- executor/tests/storage_page_accounting.rs | 7 +- .../balance/balance/balance.0_0.stdout | 2 +- .../balance_eth/balance_eth.0_0.stdout | 2 +- .../sandbox_overspend.0.stdout | 2 +- .../sandbox_overspend_2.0.stdout | 2 +- .../storage_distinct_pages.jsonnet | 7 +- .../storage_page_limit.jsonnet | 12 +- .../subtract_on_start_underflow.jsonnet | 10 +- .../message/deploy/deploy.0.stdout | 2 +- .../message/deploy_salt/deploy_salt.0.stdout | 2 +- .../internal_below_min_timeunits.jsonnet | 9 +- .../internal_below_min_timeunits.py | 5 +- .../message_count_cap.0.stdout | 2 + .../message_count_cap.1.stdout | 1 + .../message_count_cap.2.stdout | 2 + .../message_count_cap.3.stdout | 1 + .../message_count_cap.4.stdout | 2 + .../message_count_cap.5.stdout | 1 + .../message_count_cap.jsonnet | 16 + .../nested_allocation_budget.0.stdout | 2 + .../nested_allocation_budget.jsonnet | 58 +++ .../nested_allocation_budget.py | 7 + .../send_message/send_message.0.stdout | 2 +- .../send_message_eth.0.stdout | 2 +- .../send_message_on.0_0.stdout | 2 +- .../use_balance_below_min.jsonnet | 11 +- .../use_balance_below_min.py | 7 +- .../use_balance_budget_too_low.jsonnet | 7 +- .../use_balance_no_alloc.0_0_0.stdout | 2 +- .../use_balance_ok/use_balance_ok.0_0.stdout | 2 +- .../use_balance_sandbox.0_0.stdout | 2 +- .../use_balance_scaled.0_0.stdout | 2 +- .../use_balance_scaled.jsonnet | 7 +- .../use_balance_scaled/use_balance_scaled.py | 13 +- .../use_balance_zero_budget.0_0.stdout | 2 +- .../use_balance_zero_budget.jsonnet | 7 +- .../malformed_leader_public_data.0.stdout | 1 + .../malformed_leader_public_data.0_0.stdout | 1 + .../malformed_leader_public_data.jsonnet | 11 + .../output_fee_cap/output_fee_cap.0_1.stdout | 1 + .../output_fee_cap/output_fee_cap.0_2.stdout | 1 + .../output_fee_cap/output_fee_cap.0_3.stdout | 2 + .../output_fee_cap/output_fee_cap.jsonnet | 29 +- .../sandbox_fold_limit.jsonnet | 2 +- .../zero_fee_ram_bound.jsonnet | 2 +- 67 files changed, 1881 insertions(+), 511 deletions(-) create mode 100644 executor/tests/fee_bucket_accounting.rs create mode 100644 executor/tests/fee_bucket_config.rs create mode 100644 executor/tests/message_fee_external.rs create mode 100644 executor/tests/message_fee_overlay.rs create mode 100644 executor/tests/message_fee_time_units.rs create mode 100644 executor/tests/message_receipt_fees.rs create mode 100644 tests/integration/message/message_count_cap/message_count_cap.0.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.1.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.2.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.3.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.4.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.5.stdout create mode 100644 tests/integration/message/message_count_cap/message_count_cap.jsonnet create mode 100644 tests/integration/message/nested_allocation_budget/nested_allocation_budget.0.stdout create mode 100644 tests/integration/message/nested_allocation_budget/nested_allocation_budget.jsonnet create mode 100644 tests/integration/message/nested_allocation_budget/nested_allocation_budget.py create mode 100644 tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0.stdout create mode 100644 tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0_0.stdout create mode 100644 tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.jsonnet create mode 100644 tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_1.stdout create mode 100644 tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_2.stdout create mode 100644 tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_3.stdout diff --git a/docs/website/src/python-sdk/migration-guide.rst b/docs/website/src/python-sdk/migration-guide.rst index 5731cfdd..c423520d 100644 --- a/docs/website/src/python-sdk/migration-guide.rst +++ b/docs/website/src/python-sdk/migration-guide.rst @@ -91,7 +91,11 @@ VM Error Codes * - ``invalid_contract malformed_runner`` - ``invalid_contract runner malformed`` -``malformed_entry`` is new, ``out_of receipt message``, ``out_of message_fee total``, ``out_of message_fee allocation_budget`` and ``fee no_matching_allocation`` gained ``internal``/``external`` variants, and ``ResultCode.INTERNAL_ERROR`` is gone. The ``memory_limiter_consts`` and ``top_limits`` tables were removed from ``public_abi``. +``malformed_entry`` is new, ``out_of receipt message``, ``out_of message_fee total``, +``out_of message_fee allocation_budget`` and ``fee no_matching_allocation`` +gained ``internal``/``external`` variants, and ``ResultCode.INTERNAL_ERROR`` is +gone. The ``memory_limiter_consts`` and ``top_limits`` tables were removed from +``public_abi``. Storage ~~~~~~~ diff --git a/executor/crates/common/tests/fees_abi.rs b/executor/crates/common/tests/fees_abi.rs index 2c4214c6..1c31f659 100644 --- a/executor/crates/common/tests/fees_abi.rs +++ b/executor/crates/common/tests/fees_abi.rs @@ -71,11 +71,11 @@ fn internal_node( #[test] fn external_root_node_matches_exact_encoding() { let recipient = [0x11u8; 20]; - let encoded = - MessageAllocationNode::abi_encode(&[external_node(Some(recipient), None, 5, 7, 9, vec![])]); + let root = external_node(Some(recipient), None, 5, 7, 9, vec![]); + let encoded = root.abi_encode(); - // `abi.encode(MessageAllocationNode[])` of a single external root node: - // array offset, length, element offset, then the 10-word element tuple + // `abi.encode(MessageAllocationNode[])`: array offset, length, element + // offset, then the 10-word element tuple // (messageType=External, onAcceptance=false, parent=sentinel, recipient, // callKey wildcard, budget, feeParams offset, feeParams len, gasLimit, maxGasPrice). let expected = words(&[ @@ -86,12 +86,12 @@ fn external_root_node_matches_exact_encoding() { U256::from(0), // onAcceptance = false U256::MAX, // parentIndex = NODE_ROOT_SENTINEL U256::from_big_endian(&recipient), // recipient (left-padded) - U256::from(0), // callKey = CALL_KEY_WILDCARD - U256::from(5), // budget - U256::from(0xE0), // feeParams offset (7 head words) - U256::from(64), // feeParams bytes length - U256::from(7), // gasLimit - U256::from(9), // maxGasPrice + U256::from_big_endian(&genvm_modules_interfaces::fees::CALL_KEY_WILDCARD.0), + U256::from(5), // budget + U256::from(0xE0), // feeParams offset (7 head words) + U256::from(64), // feeParams bytes length + U256::from(7), // gasLimit + U256::from(9), // maxGasPrice ]); assert_eq!(encoded, expected); @@ -101,25 +101,29 @@ fn external_root_node_matches_exact_encoding() { #[test] fn nested_internal_flattens_with_parent_pointers() { - // root (internal, accepted) with a single external child. - let child = external_node(Some([0x22u8; 20]), None, 1, 100, 200, vec![]); + let grandchild = external_node(Some([0x44u8; 20]), None, 1, 100, 200, vec![]); + let first_child = external_node(Some([0x22u8; 20]), None, 2, 100, 200, vec![grandchild]); + let second_child = external_node(Some([0x33u8; 20]), None, 3, 100, 200, vec![]); let root = internal_node( genvm_modules_interfaces::On::Decided, 10, &[2, 3], - vec![child], + vec![first_child, second_child], ); - let encoded = MessageAllocationNode::abi_encode(&[root]); + let encoded = root.abi_encode(); assert_eq!(word(&encoded, 0), U256::from(0x20)); - assert_eq!(word(&encoded, 1), U256::from(2), "two flattened nodes"); + assert_eq!(word(&encoded, 1), U256::from(4), "four flattened nodes"); - // Heads region begins right after the length word (word index 2), and the + // Heads region begins right after the array length word, and the // per-element offsets there are relative to it. let heads_base = 2 * 32; - let root_idx = (heads_base + word(&encoded, 2).as_usize()) / 32; - let child_idx = (heads_base + word(&encoded, 3).as_usize()) / 32; + let element_idx = |index: usize| (heads_base + word(&encoded, 2 + index).as_usize()) / 32; + let root_idx = element_idx(0); + let first_child_idx = element_idx(1); + let second_child_idx = element_idx(2); + let grandchild_idx = element_idx(3); // Root: messageType Internal (1), onAcceptance true, parent = sentinel. assert_eq!( @@ -138,21 +142,21 @@ fn nested_internal_flattens_with_parent_pointers() { "root parent = sentinel" ); - // Child: messageType External (0), parent index = 0 (root is first flattened node). + // Both children precede the grandchild in BFS order. assert_eq!( - word(&encoded, child_idx), - U256::from(0), - "child messageType External" + word(&encoded, first_child_idx + 2), + U256::zero(), + "first child parent index 0" ); assert_eq!( - word(&encoded, child_idx + 1), + word(&encoded, second_child_idx + 2), U256::zero(), - "child onAcceptance false" + "second child parent index 0" ); assert_eq!( - word(&encoded, child_idx + 2), - U256::zero(), - "child parent index 0" + word(&encoded, grandchild_idx + 2), + U256::one(), + "grandchild parent index 1" ); } @@ -162,12 +166,13 @@ fn nested_internal_flattens_with_parent_pointers() { fn internal_params_encode_derived_appeal_rounds() { // appealRounds is not stored on the Rust side; it is reconstructed as // len(rotations) - 1 when encoding. - let encoded = MessageAllocationNode::abi_encode(&[internal_node( + let root = internal_node( genvm_modules_interfaces::On::Finalized, 10, &[2, 3, 4], vec![], - )]); + ); + let encoded = root.abi_encode(); // Walk to the feeParams bytes inside the single element. let heads_base = 2 * 32; diff --git a/executor/fuzz/genvm-storage.rs b/executor/fuzz/genvm-storage.rs index 9b76730f..7b0ec6b0 100644 --- a/executor/fuzz/genvm-storage.rs +++ b/executor/fuzz/genvm-storage.rs @@ -140,31 +140,31 @@ async fn run_storage_fuzz(input: FuzzInput) -> anyhow::Result<()> { address, genvm::rt::vm::storage::Limiter::new(sync::DArc::new( rt::fees::DataLimit::new( - vec![primitive_types::U256::MAX], + std::collections::HashMap::from([("test".to_owned(), primitive_types::U256::MAX)]), genvm::config::FeesConfig { expr_prelude: String::new(), storage: genvm::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".into(), delta_expr: r"\attrs = 0".into(), }, message_receipt: genvm::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".into(), delta_expr: r"\attrs = 0".into(), }, nondet_output: genvm::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".into(), delta_expr: r"\attrs = 0".into(), }, message_fee: genvm::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".into(), delta_expr: r"\attrs = 0".into(), }, event: genvm::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".into(), delta_expr: r"\attrs = 0".into(), }, diff --git a/executor/install/config/genvm.yaml b/executor/install/config/genvm.yaml index a51f70a5..e9e06282 100644 --- a/executor/install/config/genvm.yaml +++ b/executor/install/config/genvm.yaml @@ -9,11 +9,12 @@ modules: log_level: info fees: - # Host bucket indices (bucket_totals[] supplied by node/consensus): - # 0 - execution_data_gas budget (shared by storage, message_receipt, nondet_output, event) - # 1 - message_fee budget - # 2 - eq_outputs byte cap (node.maxEqOutputsBytes; enforced via nondet_output) - # 3 - submitted_messages byte cap (node.maxSubmittedMessagesBytes; enforced via message_receipt) + # Host buckets (bucket_totals supplied by node/consensus): + # execution_data_gas - shared by storage, message_receipt, nondet_output and event + # message_fee - outbound message fee budget + # nondet_outputs - byte cap enforced via nondet_output + # submitted_messages - byte cap enforced via message_receipt + # submitted_messages_count - message count cap enforced via message_receipt # # # Required host-provided `node` fields (from gas_data): @@ -24,18 +25,17 @@ fees: # node.bootloaderOverhead - per-tx bootloader overhead # node.fixedProposeReceiptGas - fixed cost of a propose receipt # node.fixedMessageRevealGas - fixed cost of a message reveal - # node.genPerTimeUnit - GEN per time unit (0 disables the time term) - # node.maxEqOutputsBytes - hard byte cap for eqBlocksOutputs (bucket 2) - # node.maxSubmittedMessagesBytes - hard byte cap for SubmittedMessage[] (bucket 3) + # node.lockedReceiptGasPrice - transaction's locked receipt gas price + # node.overlaySplitBps - combined developer + DAO share of the + # time-unit fee pool, in basis points + # node.receiptWrapperBytes - propose-receipt wrapper byte allowance + # node.minProposeTimeout - minimum leader timeunits allocation + # node.maxProposeTimeout - maximum leader timeunits allocation + # node.minCommitTimeout - minimum validator timeunits allocation + # node.maxCommitTimeout - maximum validator timeunits allocation # Optional: # node.validatorsPerRound[] - validator count per round # (defaults to defaultValidatorsPerRound below) - # node.minTimeUnitsPerPhase - minimum per-phase timeunits an emitted - # internal message's allocation must fund - # (leader->propose, validator->commit); a - # matched message below it is rejected at - # emission with `fee below_minimum` - # (defaults to 0 = no floor) # node.messageBudgetFloor - minimum non-zero per-round execution # budget a balance-funded internal message # may declare; the chain reverts @@ -46,7 +46,7 @@ fees: # (defaults to 0 = no floor) # # GenVM consumption points can consume multiple buckets, but as a fee only first is reported to the node - # i.e. message_receipt consumes from execution_data_gas & submitted_messages + # i.e. message_receipt consumes from execution_data_gas and both submitted message caps # to message only execution_data_gas gets attached expr_prelude: | let Y = \f = (\x = f (\v = x x v)) (\x = f (\v = x x v)) in @@ -72,16 +72,14 @@ fees: let validatorsPerRound = if hasKey node "validatorsPerRound" then node.validatorsPerRound else defaultValidatorsPerRound in - # leaf-node message fee floor = minPrimaryFees * lifecycleMultiplier + # leaf-node message fee floor = minPrimaryFees # feeParams: object { leaderTimeunitsAllocation, # validatorTimeunitsAllocation, executionBudgetPerRound, rotations[], # maxPriceGenPerTimeUnit } # appealRounds = len(rotations) - 1 - # onAcceptance: per-message lifecycle flag (bool) - # balanceFunded: true for use_balance messages (bool) - # uses validatorsPerRound[] (node or default) and, as the consensus-term - # multiplier, either the guest cap (balance-funded) or node.genPerTimeUnit. - let messageFeeFloor = \feeParams onAcceptance balanceFunded = + # uses validatorsPerRound[] (node or default) and maxPriceGenPerTimeUnit as + # the consensus-term multiplier. + let messageFeeFloor = \feeParams = let rotations = feeParams.rotations in let appealRounds = arrayLen rotations - 1 in # rounds span 0 .. 2*appealRounds and index validatorsPerRound[round]; a @@ -111,74 +109,121 @@ fees: + appealRounds in # 4. execution-budget term let executionTerm = execBudgetPerRound * leaderRounds in - # 5. consensus-term multiplier. Chain `_calculateRoundFees` charges the - # consensus term at maxPriceGenPerTimeUnit (the funding cap) for - # balance-funded messages; the allocation path keeps the historical - # node.genPerTimeUnit behaviour. Storage/receipt caps stay out of the - # floor: `feeParamsToFeesDistribution` zeroes them so their - # revert-on-exceed guards never fire during this calc. - let multiplier = if balanceFunded then feeParams.maxPriceGenPerTimeUnit else node.genPerTimeUnit in - # 6. minimum primary fees - let minPrimaryFees = - (if multiplier > 0 then multiplier * consensusTerm else consensusTerm) - + executionTerm in - # 7. lifecycle multiplier - let lifecycleMultiplier = if onAcceptance then appealRounds + 1 else 1 in - minPrimaryFees * lifecycleMultiplier + # 5. consensus-term multiplier: the chain's `_calculateRoundFees` charges + # the consensus term at maxPriceGenPerTimeUnit (the funding cap) on + # every path. Storage/receipt caps stay out of the floor: + # `feeParamsToFeesDistribution` zeroes them so their revert-on-exceed + # guards never fire during this calc. + let multiplier = feeParams.maxPriceGenPerTimeUnit in + # 6. time-unit pool and developer + DAO overlay. The overlay is grossed + # up on the time-unit pool only; integer division matches Solidity + let timeUnitPool = + if multiplier > 0 then multiplier * consensusTerm else consensusTerm in + # the overlay is a share of the gross pool, so bps must stay below 100%; + # at or above it the gross-up denominator is zero or negative, which would + # silently underprice the message instead of failing + if node.overlaySplitBps >= 10000 + then internalError "node.overlaySplitBps must be below 10000" + else + let overlaySplit = + idiv (timeUnitPool * node.overlaySplitBps) (10000 - node.overlaySplitBps) in + # 7. minimum primary fees + let minPrimaryFees = timeUnitPool + overlaySplit + executionTerm in + minPrimaryFees in storage: - bucket_no: 0 + buckets: execution_data_gas subtract_on_start_expr: | 0 delta_expr: | \a = a.pages * node.storageUnitPrice message_receipt: - # bucket 0: gas cost; bucket 3: canonical ABI byte size of this SubmittedMessage - bucket_no: [0, 3] + # Gas cost and conservative ABI byte size of this SubmittedMessage + buckets: [execution_data_gas, submitted_messages, submitted_messages_count] subtract_on_start_expr: | - [node.fixedProposeReceiptGas # for propose + [ # execution_data_gas + node.fixedProposeReceiptGas # for propose + node.intrinsicGas + node.bootloaderOverhead + 7 * node.gasPerChangedSlot - + node.fixedMessageRevealGas # for reveal - + node.intrinsicGas - + node.bootloaderOverhead - + 32 * node.gasPerChangedSlot, # for 32 bytes of length - 0] + # submitted_messages + , 0 + # submitted_messages_count + , 0 + ] delta_expr: | \a = - # each `bytes` field is indirection + length + data, rounded up to 32 bytes + let revealGas = if a.isFirstMessage then + node.fixedMessageRevealGas + + node.intrinsicGas + + node.bootloaderOverhead + + 64 * node.receiptGasPerByte # outer offset and array length + else 0 in + let revealBytes = if a.isFirstMessage then 64 else 0 in + # 32-byte element offset plus the 11-word SubmittedMessage head + let fixedBytes = 32 + 11 * 32 in + # Each `bytes` field is length + data, rounded up to 32 bytes. The extra + # calldata word conservatively covers the internal RLP wrapper let calldataBytes = 32 + 32 + ceilDiv a.calldataLength 32 * 32 in - # allocationSubtree the leader carries in the receipt under commitment modes - # (empty for external messages -> just the 64-byte indirection + length) + # allocationSubtree is empty for external and balance-funded messages let subtreeBytes = 32 + 32 + ceilDiv a.subtreeLength 32 * 32 in - # deployed contract code carried in the receipt (deploys only) + # Internal fee params have an outer offset, 8-word head, and rotations + # array; external fee params are a static 2-word tuple + let feeParamsLength = if a.isInternal then 32 + 8 * 32 + 32 + 32 * a.rotationsCount else 2 * 32 in + let feeParamsBytes = 32 + ceilDiv feeParamsLength 32 * 32 in + # Deploy code shares the internal RLP data field; charging it separately + # is conservative and avoids depending on RLP prefix lengths let codeBytes = if a.isDeploy then 32 + 32 + ceilDiv a.codeLength 32 * 32 else 0 in - let abiBytes = calldataBytes + subtreeBytes + codeBytes in - [(calldataBytes + subtreeBytes + codeBytes) * node.receiptGasPerByte + 1 * node.gasPerChangedSlot, - abiBytes] + let abiBytes = fixedBytes + calldataBytes + feeParamsBytes + subtreeBytes + codeBytes in + [ # execution_data_gas + revealGas + abiBytes * node.receiptGasPerByte + node.gasPerChangedSlot + # submitted_messages + , revealBytes + abiBytes + # submitted_messages_count + , 1] nondet_output: - # bucket 0: gas cost; bucket 2: raw output byte count - bucket_no: [0, 2] + # Gas cost and conservative compact LeaderPublicData size + buckets: [execution_data_gas, nondet_outputs] subtract_on_start_expr: | - [32 * node.receiptGasPerByte, # for 32 bytes of length - 0] + # Conservative fixed allowance for the LeaderPublicData envelope + let nondet_outputs_header_bytes = 64 in + [ # execution_data_gas + (node.receiptWrapperBytes + nondet_outputs_header_bytes) * node.receiptGasPerByte + # nondet_outputs + , nondet_outputs_header_bytes + ] delta_expr: | \a = - # bytes are indirection + length + data, rounded up to 32 bytes - let abiBytes = 32 + 32 + ceilDiv a.outputLength 32 * 32 in - [abiBytes * node.receiptGasPerByte, a.outputLength] + # Raw output bytes plus conservative compact-encoding overhead + let encodedBytes = 5 + a.outputLength in + [ # execution_data_gas + encodedBytes * node.receiptGasPerByte + # nondet_outputs + , encodedBytes + ] message_fee: - bucket_no: 1 + buckets: message_fee subtract_on_start_expr: | 0 delta_expr: | \a = - # per-phase timeunit floor (leader->propose, validator->commit): a matched - # internal message whose child timeunits fall below the node minimum would - # revert PhaseTimeoutOutOfBounds at child creation on-chain, so reject it at - # emission instead of silently dropping it. Absent constant => no floor. - let minTimeUnits = if hasKey node "minTimeUnitsPerPhase" then node.minTimeUnitsPerPhase else 0 in + # `a.matchedFeeParams` is shaped by the branch: internal params here, + # `{gasLimit, maxGasPrice}` on the external one. The bindings below read + # internal-only keys and stay correct because `let` is call-by-need -- + # the external branch never forces them. Do not hoist them past the `if`. + # Both-zero is the chain's explicit phase-timeout opt-out. Otherwise each + # allocation must fit its phase's current Idleness bounds or child creation + # reverts PhaseTimeoutOutOfBounds. + let leaderTimeUnits = a.matchedFeeParams.leaderTimeunitsAllocation in + let validatorTimeUnits = a.matchedFeeParams.validatorTimeunitsAllocation in + let phaseTimeoutsDisabled = + if leaderTimeUnits == 0 then validatorTimeUnits == 0 else false in + let phaseTimeoutsOutOfBounds = + if phaseTimeoutsDisabled then false + else if leaderTimeUnits < node.minProposeTimeout then true + else if leaderTimeUnits > node.maxProposeTimeout then true + else if validatorTimeUnits < node.minCommitTimeout then true + else validatorTimeUnits > node.maxCommitTimeout in # a non-zero per-round budget below the node floor reverts `BudgetTooLow` # at reveal on-chain; reject it at emission instead. Absent constant => no # floor. @@ -191,15 +236,16 @@ fees: else false in if a.isInternal then - if a.matchedFeeParams.leaderTimeunitsAllocation < minTimeUnits then vmError "fee below_minimum" - else if a.matchedFeeParams.validatorTimeunitsAllocation < minTimeUnits then vmError "fee below_minimum" + if phaseTimeoutsOutOfBounds then vmError "fee below_minimum" else if budgetTooLow then vmError "fee below_minimum" - else messageFeeFloor a.matchedFeeParams a.onAcceptance a.balanceFunded - # external messages reserve the worst-case L1 gas cost: gasLimit * maxGasPrice - # (matches the chain's `budget == k * (gasLimit * maxGasPrice)` requirement) - else a.matchedFeeParams.gasLimit * a.matchedFeeParams.maxGasPrice + else messageFeeFloor a.matchedFeeParams + # external messages reserve gasLimit at the effective chain price + else a.matchedFeeParams.gasLimit + * (if node.lockedReceiptGasPrice < a.matchedFeeParams.maxGasPrice + then node.lockedReceiptGasPrice + else a.matchedFeeParams.maxGasPrice) event: - bucket_no: 0 + buckets: execution_data_gas subtract_on_start_expr: | 0 delta_expr: | diff --git a/executor/src/config.rs b/executor/src/config.rs index 6483f3d5..f75f5e0a 100644 --- a/executor/src/config.rs +++ b/executor/src/config.rs @@ -15,7 +15,7 @@ fn default_fee_expr_zero() -> String { "0".to_owned() } -fn deserialize_bucket_nos<'de, D>(d: D) -> Result, D::Error> +fn deserialize_bucket_names<'de, D>(d: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { @@ -23,24 +23,26 @@ where struct Visitor; impl<'de> de::Visitor<'de> for Visitor { - type Value = Vec; + type Value = Vec; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("an integer or array of integers") + f.write_str("a non-empty string or array of non-empty strings") } - fn visit_u64(self, v: u64) -> Result, E> { - u8::try_from(v) - .map(|b| vec![b]) - .map_err(|_| E::custom(format!("bucket_no {v} exceeds u8 range"))) - } - fn visit_i64(self, v: i64) -> Result, E> { - u8::try_from(v) - .map(|b| vec![b]) - .map_err(|_| E::custom(format!("bucket_no {v} out of u8 range"))) + fn visit_str(self, v: &str) -> Result { + if v.is_empty() { + return Err(E::custom("bucket name must not be empty")); + } + Ok(vec![symbol_table::GlobalSymbol::from(v)]) } - fn visit_seq>(self, mut seq: A) -> Result, A::Error> { + fn visit_seq>(self, mut seq: A) -> Result { let mut v = Vec::new(); - while let Some(n) = seq.next_element::()? { - v.push(n); + while let Some(name) = seq.next_element::()? { + if name.is_empty() { + return Err(de::Error::custom("bucket name must not be empty")); + } + v.push(symbol_table::GlobalSymbol::from(name)); + } + if v.is_empty() { + return Err(de::Error::custom("buckets must have at least one entry")); } Ok(v) } @@ -49,9 +51,10 @@ where } #[derive(Clone, Deserialize, Debug)] +#[serde(deny_unknown_fields)] pub struct FeesBucketConfig { - #[serde(deserialize_with = "deserialize_bucket_nos")] - pub bucket_no: Vec, + #[serde(deserialize_with = "deserialize_bucket_names")] + pub buckets: Vec, /// Cost charged once, up-front, when the bucket is created /// (the fixed part of `start + sum of per-change`). #[serde(default = "default_fee_expr_zero")] diff --git a/executor/src/exe/run.rs b/executor/src/exe/run.rs index bc2376c4..a9e9c7ba 100644 --- a/executor/src/exe/run.rs +++ b/executor/src/exe/run.rs @@ -15,11 +15,16 @@ const EXECUTION_DATA_HELP: &str = "path to file containing encoded execution dat fn fill_nested_fee_buckets( is_nested: bool, - max_bucket_no: usize, - bucket_totals: &mut Vec, + bucket_names: &[symbol_table::GlobalSymbol], + bucket_totals: &mut std::collections::HashMap, ) { if is_nested && bucket_totals.is_empty() { - bucket_totals.resize(max_bucket_no + 1, primitive_types::U256::zero()); + bucket_totals.extend( + bucket_names + .iter() + .copied() + .map(|name| (name.as_str().to_owned(), primitive_types::U256::zero())), + ); } } @@ -177,7 +182,8 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> { let mut bucket_totals = execution_data .bucket_totals .iter() - .map(|bi| { + .map(|(name, bi)| { + anyhow::ensure!(!name.is_empty(), "bucket name must not be empty"); let (sign, bytes) = bi.to_bytes_be(); anyhow::ensure!( sign != num_bigint::Sign::Minus, @@ -187,30 +193,31 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> { let mut buf = [0u8; 32]; let start = 32usize.saturating_sub(bytes.len()); buf[start..].copy_from_slice(&bytes); - Ok(primitive_types::U256::from_big_endian(&buf)) + Ok((name.clone(), primitive_types::U256::from_big_endian(&buf))) }) - .collect::>>()?; - - let max_bucket_no = [ - &config.fees.storage.bucket_no, - &config.fees.message_receipt.bucket_no, - &config.fees.nondet_output.bucket_no, - &config.fees.message_fee.bucket_no, - &config.fees.event.bucket_no, + .collect::>>()?; + + let bucket_names = [ + &config.fees.storage.buckets, + &config.fees.message_receipt.buckets, + &config.fees.nondet_output.buckets, + &config.fees.message_fee.buckets, + &config.fees.event.buckets, ] .into_iter() .flat_map(|v| v.iter().copied()) - .max() - .unwrap_or(0); + .collect::>(); // A nested CallContract is read-only and receives no fee buckets. Keep the // configured bucket shape valid without granting it a spendable balance. - fill_nested_fee_buckets(is_nested, max_bucket_no.into(), &mut bucket_totals); - anyhow::ensure!( - usize::from(max_bucket_no) < bucket_totals.len(), - "fees config references bucket {max_bucket_no} but only {} bucket(s) provided", - bucket_totals.len(), - ); + fill_nested_fee_buckets(is_nested, &bucket_names, &mut bucket_totals); + for name in bucket_names { + anyhow::ensure!( + bucket_totals.contains_key(name.as_str()), + "fees config references missing bucket `{}`", + name.as_str(), + ); + } let emit_leader_public_data = !args.sync && !is_nested && execution_data.leader_public_data.is_none(); @@ -218,7 +225,7 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> { match execution_data.leader_public_data.as_ref() { None => (None, false), Some(encoded) => match genvm::leader_public_data::LeaderPublicData::decode(encoded) { - Ok(data) => (Some(data.nondet_block_outputs), false), + Ok(data) => (Some(data.nd_outs), false), Err(_) => (Some(Vec::new()), true), }, }; @@ -270,7 +277,9 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> { } let setup_run_ok = if malformed_leader_public_data { - Some(genvm::rt::vm::RunOk::VMError( + // Fatal like every other leader fault: a nested parent must not be able + // to catch it and carry on as if its child had merely failed + Some(genvm::rt::vm::RunOk::FatalVMError( genvm::public_abi::VmError::leader_fault() .nondet_output() .malformed(), @@ -307,14 +316,21 @@ pub fn handle(args: Args, mut config: config::Config) -> Result<()> { let data_fees_consumed = runtime.block_on(shared_data.data_fees_limit.consumed()); let leader_public_data = if emit_leader_public_data { genvm::leader_public_data::LeaderPublicData { - nondet_block_outputs: Vec::new(), + nd_outs: Vec::new(), } .encode() } else { bytes::Bytes::new() }; + // The normal path coalesces fatality at the top-level boundary; a + // setup-time result bypasses it, so do the same here + let mut vm_result = genvm::rt::vm::FullResult::empty_from(setup_run_ok); + if !is_nested { + vm_result.coalesce_fatal_for_top_level(); + } + let result: Result = Ok(genvm::host::FullResult::new( - genvm::rt::vm::FullResult::empty_from(setup_run_ok), + vm_result, leader_public_data, None, data_fees_remaining, diff --git a/executor/src/host/mod.rs b/executor/src/host/mod.rs index 2b64265f..93eec875 100644 --- a/executor/src/host/mod.rs +++ b/executor/src/host/mod.rs @@ -244,7 +244,7 @@ impl FullResult { emissions: Vec::new(), nondet_disagreement: None, leader_public_data: bytes::Bytes::new(), - data_fees_remaining: Vec::new(), + data_fees_remaining: std::collections::BTreeMap::new(), data_fees_consumed: genvm_modules_interfaces::BucketsConsumed::default(), llm_consumed_gen_wei: primitive_types::U256::zero(), }, @@ -258,7 +258,7 @@ impl FullResult { rt_result: rt::vm::FullResult, leader_public_data: bytes::Bytes, nondet_disagreement: Option, - data_fees_remaining: Vec, + data_fees_remaining: std::collections::BTreeMap, data_fees_consumed: rt::fees::BucketsConsumed, llm_consumption: primitive_types::U256, recorded_actions: Vec, @@ -267,7 +267,7 @@ impl FullResult { backtrace: &'a Option, data: &'a calldata::unparsed::Maybe, data_fees_consumed: &'a rt::fees::BucketsConsumed, - data_fees_remaining: &'a Vec, + data_fees_remaining: &'a std::collections::BTreeMap, emissions: &'a Vec, kind: &'a host_fns::ResultCode, wasm_store_hashes: &'a rt::errors::WasmStoreHashes, @@ -1068,7 +1068,7 @@ mod tests { rt_result, bytes::Bytes::new(), None, - Vec::new(), + std::collections::BTreeMap::new(), rt::fees::BucketsConsumed::default(), primitive_types::U256::zero(), Vec::new(), @@ -1110,7 +1110,7 @@ mod tests { rt_result, bytes::Bytes::new(), None, - Vec::new(), + std::collections::BTreeMap::new(), rt::fees::BucketsConsumed::default(), primitive_types::U256::zero(), Vec::new(), diff --git a/executor/src/leader_public_data.rs b/executor/src/leader_public_data.rs index d3c0168b..ca371544 100644 --- a/executor/src/leader_public_data.rs +++ b/executor/src/leader_public_data.rs @@ -1,126 +1,84 @@ use bytes::Bytes; +use genvm_common::internal_constants::top_limits; -const PADDING: &[u8] = b"padded"; - -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, genlayer_calldata::Encode)] pub struct LeaderPublicData { - pub nondet_block_outputs: Vec, + pub nd_outs: Vec, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DecodeError; - -impl LeaderPublicData { - pub fn encode(&self) -> Bytes { - let mut payload = Vec::new(); - for output in self - .nondet_block_outputs - .iter() - .map(Bytes::as_ref) - .chain(std::iter::once(PADDING)) - { - encode_bytes(&mut payload, output); - } - - let mut encoded = Vec::new(); - encode_len(&mut encoded, payload.len(), 0xc0, 0xf7); - encoded.extend_from_slice(&payload); - encoded.into() - } - - pub fn decode(encoded: &[u8]) -> Result { - if encoded.is_empty() { - return Ok(Self { - nondet_block_outputs: Vec::new(), - }); - } - - let (payload_start, payload_len) = decode_len(encoded, 0, true)?; - let payload_end = payload_start.checked_add(payload_len).ok_or(DecodeError)?; - if payload_end != encoded.len() { - return Err(DecodeError); - } - - let mut cursor = payload_start; - let mut outputs = Vec::new(); - while cursor < payload_end { - let (data_start, data_len) = decode_len(encoded, cursor, false)?; - let data_end = data_start.checked_add(data_len).ok_or(DecodeError)?; - if data_end > payload_end { - return Err(DecodeError); +impl genlayer_calldata::codec::Decode for LeaderPublicData { + fn decode( + deserializer: D, + ) -> Result { + use genlayer_calldata::codec::{DecodeError, MapAccess, SeqAccess, Visitor}; + + struct OutputsVisitor; + impl Visitor for OutputsVisitor { + type Value = Vec; + + fn visit_seq( + self, + len: u64, + mut seq: A, + ) -> Result { + if len > u64::from(top_limits::NONDET_BLOCKS) { + return Err(DecodeError::Custom( + "too many nondeterministic outputs".to_owned(), + )); + } + + let mut outputs = Vec::with_capacity(len as usize); + while let Some(output) = seq.next_element::()? { + outputs.push(output); + } + debug_assert_eq!(outputs.len(), len as usize); + Ok(outputs) } - outputs.push(Bytes::copy_from_slice(&encoded[data_start..data_end])); - cursor = data_end; } - if outputs.last().is_none_or(|last| last.as_ref() != PADDING) { - return Err(DecodeError); + struct LeaderPublicDataVisitor; + impl Visitor for LeaderPublicDataVisitor { + type Value = LeaderPublicData; + + fn visit_map( + self, + len: u64, + mut map: A, + ) -> Result { + if len != 1 { + return Err(DecodeError::LengthMismatch { + expected: 1, + got: usize::try_from(len).unwrap_or(usize::MAX), + }); + } + let Some(key) = map.next_key()? else { + return Err(DecodeError::FieldMissing("nd_outs")); + }; + if key != "nd_outs" { + return Err(DecodeError::UnknownField(key.to_owned())); + } + + let nd_outs = map.next_value_visit(OutputsVisitor)?; + debug_assert!(map.next_key()?.is_none()); + Ok(LeaderPublicData { nd_outs }) + } } - outputs.pop(); - Ok(Self { - nondet_block_outputs: outputs, - }) + deserializer.deserialize(LeaderPublicDataVisitor) } } -fn encode_bytes(output: &mut Vec, value: &[u8]) { - if value.len() == 1 && value[0] < 0x80 { - output.push(value[0]); - return; - } - - encode_len(output, value.len(), 0x80, 0xb7); - output.extend_from_slice(value); -} - -fn encode_len(output: &mut Vec, len: usize, short_base: u8, long_base: u8) { - if len <= 55 { - output.push(short_base + len as u8); - return; - } - - let bytes = len.to_be_bytes(); - let first = bytes.iter().position(|byte| *byte != 0).unwrap(); - let len_bytes = &bytes[first..]; - output.push(long_base + len_bytes.len() as u8); - output.extend_from_slice(len_bytes); -} - -fn decode_len(encoded: &[u8], offset: usize, list: bool) -> Result<(usize, usize), DecodeError> { - let prefix = *encoded.get(offset).ok_or(DecodeError)?; - let short_base: u8 = if list { 0xc0 } else { 0x80 }; - let long_base: u8 = if list { 0xf7 } else { 0xb7 }; - - if !list && prefix < 0x80 { - return Ok((offset, 1)); - } - if prefix < short_base || prefix > long_base.saturating_add(size_of::() as u8) { - return Err(DecodeError); - } - if prefix <= long_base { - if !list && prefix == 0x81 && encoded.get(offset + 1).is_some_and(|byte| *byte < 0x80) { - return Err(DecodeError); - } - return Ok((offset + 1, usize::from(prefix - short_base))); - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DecodeError; - let len_len = usize::from(prefix - long_base); - let len_start = offset.checked_add(1).ok_or(DecodeError)?; - let len_end = len_start.checked_add(len_len).ok_or(DecodeError)?; - let len_bytes = encoded.get(len_start..len_end).ok_or(DecodeError)?; - if len_bytes.first() == Some(&0) { - return Err(DecodeError); +impl LeaderPublicData { + pub fn encode(&self) -> Bytes { + genlayer_calldata::encode_obj(self).into() } - let mut buf = [0; size_of::()]; - buf[size_of::() - len_len..].copy_from_slice(len_bytes); - let len = usize::from_be_bytes(buf); - if len <= 55 { - return Err(DecodeError); + pub fn decode(encoded: &[u8]) -> Result { + genlayer_calldata::decode_obj(encoded).map_err(|_| DecodeError) } - - Ok((len_end, len)) } #[cfg(test)] @@ -128,40 +86,53 @@ mod tests { use super::*; #[test] - fn rlp_round_trip() { + fn calldata_round_trip() { let data = LeaderPublicData { - nondet_block_outputs: vec![Bytes::from_static(b"a"), Bytes::from_static(b"bc")], + nd_outs: vec![Bytes::from_static(b"a"), Bytes::from_static(b"bc")], }; assert_eq!(LeaderPublicData::decode(&data.encode()), Ok(data)); } #[test] - fn preserves_legacy_encoding() { + fn has_stable_calldata_encoding() { let data = LeaderPublicData { - nondet_block_outputs: vec![Bytes::from_static(b"test")], + nd_outs: vec![Bytes::from_static(b"a"), Bytes::from_static(b"bc")], }; - assert_eq!(data.encode().as_ref(), b"\xcc\x84test\x86padded"); + assert_eq!(data.encode().as_ref(), b"\x0e\x07nd_outs\x15\x0ba\x13bc"); } #[test] - fn empty_timeout_decodes_as_no_outputs() { + fn rejects_empty_legacy_and_trailing_data() { + assert_eq!(LeaderPublicData::decode(&[]), Err(DecodeError)); assert_eq!( - LeaderPublicData::decode(&[]), - Ok(LeaderPublicData { - nondet_block_outputs: Vec::new() - }) + LeaderPublicData::decode(b"\xcc\x84test\x86padded"), + Err(DecodeError) ); + + let mut encoded = LeaderPublicData { + nd_outs: Vec::new(), + } + .encode() + .to_vec(); + encoded.push(0); + assert_eq!(LeaderPublicData::decode(&encoded), Err(DecodeError)); } #[test] - fn rejects_noncanonical_rlp() { - assert_eq!(LeaderPublicData::decode(b"\xc0"), Err(DecodeError)); + fn bounds_output_count_while_decoding() { + let at_limit = LeaderPublicData { + nd_outs: vec![Bytes::new(); top_limits::NONDET_BLOCKS as usize], + }; + assert_eq!(LeaderPublicData::decode(&at_limit.encode()), Ok(at_limit)); + + let above_limit = LeaderPublicData { + nd_outs: vec![Bytes::new(); top_limits::NONDET_BLOCKS as usize + 1], + }; assert_eq!( - LeaderPublicData::decode(b"\xc7\x86padded\x00"), + LeaderPublicData::decode(&above_limit.encode()), Err(DecodeError) ); - assert_eq!(LeaderPublicData::decode(b"\xc2\x81\x01"), Err(DecodeError)); } } diff --git a/executor/src/lib.rs b/executor/src/lib.rs index be63b0ac..440814a4 100644 --- a/executor/src/lib.rs +++ b/executor/src/lib.rs @@ -487,6 +487,10 @@ pub async fn run_with_impl( data_fees_limit, messages_value_decremented: primitive_types::U256::zero(), emissions: Vec::new(), + message_fee_allocation_consumed: vec![ + primitive_types::U256::zero(); + entry_data.message_fee_allocation.len() + ], message_fee_allocation: entry_data.message_fee_allocation, }, det_subvm_hashes: Default::default(), @@ -622,7 +626,7 @@ pub async fn run_with( && supervisor.emit_leader_public_data { leader_public_data::LeaderPublicData { - nondet_block_outputs: nondet_results, + nd_outs: nondet_results, } .encode() } else { diff --git a/executor/src/rt/fees.rs b/executor/src/rt/fees.rs index 2f56dc62..31fa0264 100644 --- a/executor/src/rt/fees.rs +++ b/executor/src/rt/fees.rs @@ -58,9 +58,8 @@ pub fn fee_params_value_internal( let rotations: Vec = p.rotations.iter().map(|r| num_u256(*r)).collect(); m.insert("rotations".to_owned(), Value::Array(Arc::new(rotations))); // v0.6-dev (CON-549) price caps. `maxPriceGenPerTimeUnit` is the funding - // multiplier for balance-funded messages (chain `_calculateRoundFees`); the - // storage/receipt caps are exposed for completeness but stay out of the floor - // calc, mirroring `feeParamsToFeesDistribution` which zeroes them there. + // multiplier for all internal messages (chain `_calculateRoundFees`); the + // storage/receipt caps stay out of the floor calculation. m.insert( "maxPriceGenPerTimeUnit".to_owned(), num_u256(p.max_price_gen_per_time_unit), @@ -120,7 +119,7 @@ fn value_to_u256_vec( genvm_common::expr::Value::Array(arr) => { rt::errors::internal_ensure!( arr.len() == bucket_count, - "fee expression returned array of length {} but bucket_no has {bucket_count} entries", + "fee expression returned array of length {} but buckets has {bucket_count} entries", arr.len(), ); arr.iter() @@ -161,12 +160,12 @@ fn eval_with_node( /// [`DataLimit::consume_initial`]) + Σ `delta(attrs)`. `delta` is a function /// closing over `node`/the prelude. /// -/// `bucket_nos` can target multiple on-chain buckets. When the delta expression +/// `bucket_names` can target multiple on-chain buckets. When the delta expression /// returns a scalar it is charged identically against every bucket; when it /// returns an array the lengths must match and each element is charged to the /// corresponding bucket. All subtractions are atomic (all-or-nothing). struct Bucket { - bucket_nos: Vec, + bucket_names: Vec, subtract_on_start: Vec, delta: genvm_common::expr::Value, oom_error: abi::consts::VmError, @@ -177,7 +176,14 @@ struct Bucket { impl std::fmt::Debug for Bucket { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Bucket") - .field("bucket_nos", &self.bucket_nos) + .field( + "bucket_names", + &self + .bucket_names + .iter() + .map(symbol_table::GlobalSymbol::as_str) + .collect::>(), + ) .field("subtract_on_start", &self.subtract_on_start) .finish() } @@ -189,8 +195,8 @@ fn build_bucket( node: &genvm_common::expr::Value, oom_error: abi::consts::VmError, ) -> rt::errors::Result { - let n = cfg.bucket_no.len(); - rt::errors::internal_ensure!(n > 0, "bucket_no must have at least one entry"); + let n = cfg.buckets.len(); + rt::errors::internal_ensure!(n > 0, "buckets must have at least one entry"); let subtract_on_start = value_to_u256_vec( eval_with_node( @@ -203,8 +209,9 @@ fn build_bucket( )?; let delta = eval_with_node(prelude, "delta", &cfg.delta_expr, node)?; + debug_assert_eq!(subtract_on_start.len(), n); Ok(Bucket { - bucket_nos: cfg.bucket_no.clone(), + bucket_names: cfg.buckets.clone(), subtract_on_start, delta, oom_error, @@ -246,8 +253,10 @@ pub struct BucketsConsumed { #[derive(Debug, Clone, Copy)] pub struct MessageReceiptParams { + pub is_first_message: bool, pub is_internal: bool, pub is_deploy: bool, + pub rotations_count: u64, pub calldata_length: u64, pub code_length: u64, pub subtree_length: u64, @@ -255,7 +264,7 @@ pub struct MessageReceiptParams { #[derive(Debug)] pub struct DataLimit { - buckets: tokio::sync::Mutex>, + buckets: tokio::sync::Mutex>, storage: Bucket, message_receipt: Bucket, nondet_output: Bucket, @@ -265,7 +274,7 @@ pub struct DataLimit { impl DataLimit { pub fn new( - bucket_totals: Vec, + bucket_totals: std::collections::HashMap, fees: crate::config::FeesConfig, gas_data: std::collections::BTreeMap, ) -> rt::errors::Result { @@ -315,6 +324,24 @@ impl DataLimit { abi::consts::VmError::out_of().receipt().event(), )?; + for bucket in [ + &storage, + &message_receipt, + &nondet_output, + &message_fee, + &event, + ] { + debug_assert_eq!(bucket.bucket_names.len(), bucket.subtract_on_start.len()); + debug_assert_eq!(bucket.bucket_names.len(), bucket.total_consumed.len()); + for &name in &bucket.bucket_names { + rt::errors::internal_ensure!( + bucket_totals.contains_key(name.as_str()), + "fees config references missing bucket `{}`", + name.as_str(), + ); + } + } + Ok(Self { buckets: tokio::sync::Mutex::new(bucket_totals), storage, @@ -338,8 +365,11 @@ impl DataLimit { } Err(e) => Err(rt::errors::Error::internal(format!("{}", e))), }; - match res.and_then(|v| value_to_u256_vec(v, bucket.bucket_nos.len())) { - Ok(costs) => Ok(CostVec(costs)), + match res.and_then(|v| value_to_u256_vec(v, bucket.bucket_names.len())) { + Ok(costs) => { + debug_assert_eq!(costs.len(), bucket.bucket_names.len()); + Ok(CostVec(costs)) + } Err(e) => { log_error!(error:err = e; "failed to evaluate fee expression"); Err(e).ctx("failed to evaluate fee expression") @@ -357,16 +387,26 @@ impl DataLimit { } async fn consume_bucket_raw(&self, bucket: &Bucket, costs: &[primitive_types::U256]) -> bool { + debug_assert_eq!(bucket.bucket_names.len(), costs.len()); + debug_assert_eq!(bucket.bucket_names.len(), bucket.total_consumed.len()); let mut buckets = self.buckets.lock().await; if !Self::bucket_costs_fit(&buckets, bucket, costs) { return false; } - for (i, (&bno, &cost)) in bucket.bucket_nos.iter().zip(costs.iter()).enumerate() { - buckets[usize::from(bno)] -= cost; + for (i, (&name, &cost)) in bucket.bucket_names.iter().zip(costs.iter()).enumerate() { + let Some(remaining) = buckets.get_mut(name.as_str()) else { + debug_assert!( + buckets.contains_key(name.as_str()), + "validated bucket disappeared: {}", + name.as_str() + ); + return false; + }; + *remaining -= cost; log_debug!( - bucket = bno, + bucket = name.as_str(), cost:display = cost, - remaining:display = buckets[usize::from(bno)]; + remaining:display = *remaining; "consume_bucket: ok" ); *bucket.total_consumed[i].lock().await += cost; @@ -376,36 +416,50 @@ impl DataLimit { } fn bucket_costs_fit( - buckets: &[primitive_types::U256], + buckets: &std::collections::HashMap, bucket: &Bucket, costs: &[primitive_types::U256], ) -> bool { - for (idx, (&bno, &cost)) in bucket.bucket_nos.iter().zip(costs.iter()).enumerate() { - let Some(remaining) = buckets.get(usize::from(bno)) else { - log_warn!(bucket = bno; "consume_bucket: bucket index out of range"); + debug_assert_eq!(bucket.bucket_names.len(), costs.len()); + for (idx, (&name, &cost)) in bucket.bucket_names.iter().zip(costs.iter()).enumerate() { + let Some(remaining) = buckets.get(name.as_str()) else { + debug_assert!( + buckets.contains_key(name.as_str()), + "validated bucket disappeared: {}", + name.as_str() + ); + log_warn!(bucket = name.as_str(); "consume_bucket: bucket missing"); return false; }; if *remaining < cost { log_warn!( - bucket = bno, + bucket = name.as_str(), cost:display = cost, remaining:display = *remaining; "consume_bucket: insufficient funds" ); return false; } - // when the same bucket_no appears more than once, verify + // When the same bucket appears more than once, verify // cumulative cost fits let mut cumulative = cost; - for (&prev_bno, &prev_cost) in bucket.bucket_nos[..idx].iter().zip(costs[..idx].iter()) + for (&prev_name, &prev_cost) in + bucket.bucket_names[..idx].iter().zip(costs[..idx].iter()) { - if prev_bno == bno { - cumulative += prev_cost; + if prev_name == name { + let Some(total) = cumulative.checked_add(prev_cost) else { + log_warn!( + bucket = name.as_str(); + "consume_bucket: cumulative cost overflow" + ); + return false; + }; + cumulative = total; } } if *remaining < cumulative { log_warn!( - bucket = bno, + bucket = name.as_str(), cumulative:display = cumulative, remaining:display = *remaining; "consume_bucket: insufficient funds (cumulative)" @@ -426,8 +480,13 @@ impl DataLimit { Ok(Self::bucket_costs_fit(&buckets, bucket, &costs.0)) } - pub async fn remaining(&self) -> Vec { - self.buckets.lock().await.clone() + pub async fn remaining(&self) -> std::collections::BTreeMap { + self.buckets + .lock() + .await + .iter() + .map(|(name, total)| (name.clone(), *total)) + .collect() } async fn sum_consumed(bucket: &Bucket) -> primitive_types::U256 { @@ -468,7 +527,11 @@ impl DataLimit { bucket.oom_error.clone(), rt::errors::internal!( "subtract_on_start exceeds bucket {:?} total", - bucket.bucket_nos + bucket + .bucket_names + .iter() + .map(symbol_table::GlobalSymbol::as_str) + .collect::>() ), )); } @@ -488,8 +551,10 @@ impl DataLimit { self.calculate_bucket( &self.message_receipt, &[ + ("isFirstMessage", params.is_first_message.into()), ("isInternal", params.is_internal.into()), ("isDeploy", params.is_deploy.into()), + ("rotationsCount", num(params.rotations_count)), ("calldataLength", num(params.calldata_length)), ("codeLength", num(params.code_length)), ("subtreeLength", num(params.subtree_length)), @@ -535,22 +600,14 @@ impl DataLimit { } } - /// `balance_funded` selects the chain's `minMessagePrimaryFees` multiplier: - /// balance-funded (`useBalance`) messages charge the consensus term at the - /// guest's `maxPriceGenPerTimeUnit` cap (per `_calculateRoundFees`), whereas - /// allocation-matched messages use the node's live `genPerTimeUnit`. pub fn calculate_message_fee_internal( &self, - on: abi::gl_call::On, - balance_funded: bool, matched_fee_params: &genlayer_sdk::abi::fees::InternalMessageParams, ) -> rt::errors::Result { self.calculate_bucket( &self.message_fee, &[ ("isInternal", true.into()), - ("onAcceptance", (on == abi::gl_call::On::Decided).into()), - ("balanceFunded", balance_funded.into()), ( "matchedFeeParams", fee_params_value_internal(matched_fee_params), @@ -563,31 +620,61 @@ impl DataLimit { pub async fn consume_message_fee(&self, cost_fee: &CostVec, cost_receipt: &CostVec) -> bool { let mut buckets = self.buckets.lock().await; - // Build a cumulative deduction map: bucket_index -> total to subtract. - let mut deductions: std::collections::BTreeMap = - std::collections::BTreeMap::new(); - - for (&bno, &cost) in self.message_fee.bucket_nos.iter().zip(cost_fee.0.iter()) { - *deductions.entry(bno).or_default() += cost; + debug_assert_eq!(self.message_fee.bucket_names.len(), cost_fee.0.len()); + debug_assert_eq!( + self.message_receipt.bucket_names.len(), + cost_receipt.0.len() + ); + let mut deductions: Vec<(symbol_table::GlobalSymbol, primitive_types::U256)> = Vec::new(); + + for (&name, &cost) in self.message_fee.bucket_names.iter().zip(cost_fee.0.iter()) { + if let Some((_, total)) = deductions + .iter_mut() + .find(|(existing, _)| *existing == name) + { + let Some(sum) = total.checked_add(cost) else { + log_warn!(bucket = name.as_str(); "consume_message_fee: cost overflow"); + return false; + }; + *total = sum; + } else { + deductions.push((name, cost)); + } } - for (&bno, &cost) in self + for (&name, &cost) in self .message_receipt - .bucket_nos + .bucket_names .iter() .zip(cost_receipt.0.iter()) { - *deductions.entry(bno).or_default() += cost; + if let Some((_, total)) = deductions + .iter_mut() + .find(|(existing, _)| *existing == name) + { + let Some(sum) = total.checked_add(cost) else { + log_warn!(bucket = name.as_str(); "consume_message_fee: cost overflow"); + return false; + }; + *total = sum; + } else { + deductions.push((name, cost)); + } } // Check all buckets first (atomic: all-or-nothing). - for (&bno, &total) in &deductions { - let Some(remaining) = buckets.get(usize::from(bno)) else { - log_warn!(bucket = bno; "consume_message_fee: bucket index out of range"); + for &(name, total) in &deductions { + let Some(remaining) = buckets.get(name.as_str()) else { + debug_assert!( + buckets.contains_key(name.as_str()), + "validated bucket disappeared: {}", + name.as_str() + ); + log_warn!(bucket = name.as_str(); "consume_message_fee: bucket missing"); return false; }; if *remaining < total { log_warn!( - bucket = bno, + bucket = name.as_str(), cost:display = total, remaining:display = *remaining; "consume_message_fee: insufficient funds" @@ -597,8 +684,11 @@ impl DataLimit { } // Apply all deductions. - for (&bno, &total) in &deductions { - buckets[usize::from(bno)] -= total; + for &(name, total) in &deductions { + let remaining = buckets + .get_mut(name.as_str()) + .expect("validated fee bucket must remain present"); + *remaining -= total; } std::mem::drop(buckets); diff --git a/executor/src/wasi/genlayer_sdk/message.rs b/executor/src/wasi/genlayer_sdk/message.rs index 9742f7a7..a0970ec7 100644 --- a/executor/src/wasi/genlayer_sdk/message.rs +++ b/executor/src/wasi/genlayer_sdk/message.rs @@ -1,9 +1,96 @@ use super::*; + +fn allocation_match_priority( + node: &genvm_modules_interfaces::fees::MessageAllocationNode, + recipient: calldata::Address, + call_key: genvm_modules_interfaces::abi_stub::CallKey, +) -> Option { + let recipient_priority = match node.recipient { + Some(candidate) if candidate == recipient => 0, + Some(_) => return None, + None => 2, + }; + let call_key_priority = match node.call_key { + Some(candidate) if candidate == call_key => 0, + Some(_) => return None, + None => 1, + }; + + Some(recipient_priority + call_key_priority) +} + +fn internal_allocation_match_priority( + node: &genvm_modules_interfaces::fees::MessageAllocationNode, + recipient: calldata::Address, + call_key: genvm_modules_interfaces::abi_stub::CallKey, +) -> Option { + if node.budget.is_zero() { + return None; + } + + allocation_match_priority(node, recipient, call_key) +} + +pub(super) fn resolve_internal_allocation( + nodes: &[genvm_modules_interfaces::fees::MessageAllocationNode], + on: genvm_modules_interfaces::On, + recipient: calldata::Address, + call_key: genvm_modules_interfaces::abi_stub::CallKey, +) -> Option<( + usize, + std::sync::Arc, +)> { + let priority = nodes + .iter() + .filter(|node| { + matches!( + &node.fee_params, + genvm_modules_interfaces::fees::MessageAllocationNodeParams::Internal(_) + ) + }) + .filter_map(|node| internal_allocation_match_priority(node, recipient, call_key)) + .min()?; + let index = nodes.iter().position(|node| { + internal_allocation_match_priority(node, recipient, call_key) == Some(priority) + && node.on == on + && matches!( + &node.fee_params, + genvm_modules_interfaces::fees::MessageAllocationNodeParams::Internal(_) + ) + })?; + let node = &nodes[index]; + let params = node.matches_internal(on, recipient, call_key)?; + + Some((index, params)) +} + +pub(super) fn external_allocation_candidates( + nodes: &[genvm_modules_interfaces::fees::MessageAllocationNode], + recipient: calldata::Address, + call_key: genvm_modules_interfaces::abi_stub::CallKey, +) -> Vec { + let mut candidates = nodes + .iter() + .enumerate() + .filter(|(_, node)| { + matches!( + &node.fee_params, + genvm_modules_interfaces::fees::MessageAllocationNodeParams::External(_) + ) + }) + .filter_map(|(index, node)| { + allocation_match_priority(node, recipient, call_key).map(|priority| (priority, index)) + }) + .collect::>(); + candidates.sort_by_key(|(priority, _)| *priority); + candidates.into_iter().map(|(_, index)| index).collect() +} use crate::rt::errors::ResultExt as _; use genlayer_calldata::codec::Encode; /// Named arguments for [`consume_message_fee_internal`]. struct ConsumeInternalArgs { + is_first_message: bool, is_deploy: bool, calldata_length: u64, code_length: u64, @@ -14,7 +101,10 @@ struct ConsumeInternalArgs { enum FeeFunding<'a> { /// Sender-pool allocation: fee capped by `node.budget`; consumes the /// message-fee and receipt buckets. - Allocation(&'a mut genvm_modules_interfaces::fees::MessageAllocationNode), + Allocation { + node: &'a genvm_modules_interfaces::fees::MessageAllocationNode, + consumed: &'a mut primitive_types::U256, + }, /// Balance-funded (`useBalance`): the metered fee is the `declaredBudget`, /// reserved from the contract balance. On-chain such messages are excluded /// from the sender pool, so the message-fee bucket is skipped and only the @@ -38,6 +128,12 @@ fn convert_call_key_to_modules(call_key: abi::CallKey) -> genvm_modules_interfac genvm_modules_interfaces::CallKey(call_key.0) } +pub(super) fn next_message_is_first(emissions: &[domain::ExecutionEmission]) -> bool { + emissions + .iter() + .all(|emission| matches!(emission, domain::ExecutionEmission::Event { .. })) +} + fn convert_internal_message_params_to_sdk( params: &genvm_modules_interfaces::fees::InternalMessageParams, ) -> abi::fees::InternalMessageParams { @@ -65,21 +161,35 @@ async fn consume_message_fee_internal( shared_data: &rt::SharedData, funding: FeeFunding<'_>, fee_params: Arc, - on: gl_call::On, args: ConsumeInternalArgs, ) -> Result { - let balance_funded = matches!(funding, FeeFunding::Balance { .. }); - let fee_cost = shared_data + let mut fee_cost = shared_data .data_fees_limit - .calculate_message_fee_internal(on, balance_funded, &fee_params) + .calculate_message_fee_internal(&fee_params) .map_err(internal_trap)?; + + if let FeeFunding::Allocation { node, .. } = &funding { + let declared_budget = node + .children + .iter() + .try_fold(fee_cost.reported_fee(), |total, child| { + total.checked_add(child.budget) + }) + .ok_or_else(|| { + internal_trap(rt::errors::internal!("message declared budget overflow")) + })?; + fee_cost.0[0] = declared_budget; + } + let fee_total = fee_cost.reported_fee(); let receipt_cost = shared_data .data_fees_limit .calculate_message_receipt(rt::fees::MessageReceiptParams { + is_first_message: args.is_first_message, is_internal: true, is_deploy: args.is_deploy, + rotations_count: usize_into_u64(fee_params.rotations.len()), calldata_length: args.calldata_length, code_length: args.code_length, subtree_length: args.subtree_length, @@ -88,12 +198,17 @@ async fn consume_message_fee_internal( .map_err(internal_trap)?; match funding { - FeeFunding::Allocation(node) => { - if fee_total > node.budget { + FeeFunding::Allocation { node, consumed } => { + let remaining_budget = node.budget.checked_sub(*consumed).ok_or_else(|| { + internal_trap(rt::errors::internal!( + "message allocation consumed budget exceeds its total" + )) + })?; + if fee_total > remaining_budget { log_warn!( node:cd = *node, fee_cost:cd = fee_total, - budget: cd = node.budget; + budget: cd = remaining_budget; "message fee cost exceeds node budget" ); return Err(internal_trap(rt::errors::Error::vm( @@ -123,7 +238,7 @@ async fn consume_message_fee_internal( ))); } - node.budget -= fee_total; + *consumed += fee_total; } FeeFunding::Balance { value, @@ -172,13 +287,15 @@ async fn consume_message_fee_internal( /// Named arguments for [`consume_message_fee_external`]. struct ConsumeExternalArgs { + is_first_message: bool, is_deploy: bool, calldata_length: u64, } async fn consume_message_fee_external( shared_data: &rt::SharedData, - node: &mut genvm_modules_interfaces::fees::MessageAllocationNode, + node: &genvm_modules_interfaces::fees::MessageAllocationNode, + consumed: &mut primitive_types::U256, params: abi::fees::ExternalMessageParams, // External messages are always emitted on finalization; carried for signature // symmetry with the internal path. @@ -191,7 +308,12 @@ async fn consume_message_fee_external( .map_err(internal_trap)?; let fee_total = fee_cost.reported_fee(); - if fee_total > node.budget { + let remaining_budget = node.budget.checked_sub(*consumed).ok_or_else(|| { + internal_trap(rt::errors::internal!( + "message allocation consumed budget exceeds its total" + )) + })?; + if fee_total > remaining_budget { return Err(internal_trap(rt::errors::Error::vm( abi::consts::VmError::out_of() .message_fee() @@ -203,8 +325,10 @@ async fn consume_message_fee_external( let receipt_cost = shared_data .data_fees_limit .calculate_message_receipt(rt::fees::MessageReceiptParams { + is_first_message: args.is_first_message, is_internal: false, is_deploy: args.is_deploy, + rotations_count: 0, calldata_length: args.calldata_length, code_length: 0, subtree_length: 0, @@ -224,7 +348,7 @@ async fn consume_message_fee_external( ))); } - node.budget -= fee_total; + *consumed += fee_total; Ok(rt::fees::MessageFeeConsumption { message_fee: fee_cost, @@ -232,16 +356,49 @@ async fn consume_message_fee_external( }) } +async fn consume_external_receipt_only( + shared_data: &rt::SharedData, + args: ConsumeExternalArgs, +) -> Result { + let receipt_cost = shared_data + .data_fees_limit + .calculate_message_receipt(rt::fees::MessageReceiptParams { + is_first_message: args.is_first_message, + is_internal: false, + is_deploy: args.is_deploy, + rotations_count: 0, + calldata_length: args.calldata_length, + code_length: 0, + subtree_length: 0, + }) + .map_err(internal_trap)?; + + if !shared_data + .data_fees_limit + .consume_message_receipt_only(&receipt_cost) + .await + { + return Err(internal_trap(rt::errors::Error::vm( + abi::consts::VmError::out_of().receipt().message().val(), + ))); + } + + Ok(rt::fees::MessageFeeConsumption { + message_fee: rt::fees::CostVec(vec![primitive_types::U256::zero()]), + receipt_fee: receipt_cost, + }) +} + /// Magnitude bounds (in significant bits; a larger field is rejected) on /// guest-supplied fee params. Invariant the code cannot express: the worst-case -/// `messageFeeFloor` product must stay within U256, since the fee evaluator's +/// `messageFeeFloor` result must stay within U256, since the fee evaluator's /// `rational_to_u256` treats overflow as an internal abort. Three guest fields /// multiply into one floor term (`maxPrice × rotations entry × validatorTU`), /// so with the default 18-round validator table (counts ≤ 1537 < 2^11) the -/// accepted-lifecycle worst case is -/// lifecycle(<2^4) × [price(<2^96) × rounds(<2^5) × rot(<2^33) +/// worst case is +/// price(<2^96) × rounds(<2^5) × rot(<2^33) /// × (leaderTU + vpr × validatorTU)(<2^44) -/// + price(<2^96) × leaderRounds(<2^36)] +/// + price(<2^96) × leaderRounds(<2^36) /// < 2^183 ≪ 2^256. /// Economically generous: 2^96 atto-GEN ≈ 8e10 GEN for prices/budgets; 2^32 /// for counts (time units per phase, rotations per round). @@ -357,49 +514,100 @@ impl ContextVFS<'_> { call_key.0[..4].copy_from_slice(&calldata[..4]); } - let Some((matched_node, matched_params)) = self - .context - .data - .accumulator - .message_fee_allocation - .iter_mut() - .find_map(|node| { - node.matches_external(address, convert_call_key_to_modules(call_key)) - .map(|params| (node, params)) - }) - else { + let recipient = address; + let call_key_modules = convert_call_key_to_modules(call_key); + let candidates = external_allocation_candidates( + &self.context.data.accumulator.message_fee_allocation, + recipient, + call_key_modules, + ); + let has_candidates = !candidates.is_empty(); + let mut matched = None; + for index in candidates { + let node = &self.context.data.accumulator.message_fee_allocation[index]; + let Some(params) = node.matches_external(recipient, call_key_modules) else { + continue; + }; + let params = convert_external_message_params_to_sdk(params); + let fee = self + .context + .data + .supervisor + .shared_data + .data_fees_limit + .calculate_message_fee_external(¶ms) + .map_err(internal_trap)?; + let remaining_budget = node + .budget + .checked_sub( + self.context + .data + .accumulator + .message_fee_allocation_consumed[index], + ) + .ok_or_else(|| { + internal_trap(rt::errors::internal!( + "message allocation consumed budget exceeds its total" + )) + })?; + if fee.reported_fee() <= remaining_budget { + matched = Some((index, params)); + break; + } + } + if has_candidates && matched.is_none() { log_warn!( recipient = address, call_key:? = call_key; - "no matching node for message fee allocation" + "matching external allocations are exhausted" ); return Err(internal_trap(rt::errors::Error::vm( - abi::consts::VmError::fee() - .no_matching_allocation() + abi::consts::VmError::out_of() + .message_fee() + .allocation_budget() .external(), ))); - }; + } let calldata_length = calldata.len().into_int_comptime(); - let matched_params = convert_external_message_params_to_sdk(matched_params); let allocation = reserve_permanent( &self.context.limiter, emission_allocation_size(&[calldata_length]), "external message", )?; - let fees = consume_message_fee_external( - &self.context.data.supervisor.shared_data, - matched_node, - matched_params, - gl_call::On::Finalized, - ConsumeExternalArgs { - is_deploy: false, - calldata_length, - }, - ) - .await?; + let args = ConsumeExternalArgs { + is_first_message: next_message_is_first(&self.context.data.accumulator.emissions), + is_deploy: false, + calldata_length, + }; + let (fees, fee_params) = if let Some((matched_index, matched_params)) = matched { + let accumulator = &mut self.context.data.accumulator; + let matched_node = &accumulator.message_fee_allocation[matched_index]; + let consumed = &mut accumulator.message_fee_allocation_consumed[matched_index]; + let fees = consume_message_fee_external( + &self.context.data.supervisor.shared_data, + matched_node, + consumed, + matched_params, + gl_call::On::Finalized, + args, + ) + .await?; + (fees, matched_params) + } else { + let fees = + consume_external_receipt_only(&self.context.data.supervisor.shared_data, args) + .await?; + ( + fees, + abi::fees::ExternalMessageParams { + gas_limit: primitive_types::U256::zero(), + max_gas_price: primitive_types::U256::zero(), + }, + ) + }; self.context .data @@ -409,9 +617,9 @@ impl ContextVFS<'_> { address, calldata, value, - message_fee: fees.message_fee.reported_fee(), + message_fee: primitive_types::U256::zero(), receipt_fee: fees.receipt_fee.reported_fee(), - fee_params: matched_params, + fee_params, }); allocation.commit(); @@ -545,6 +753,7 @@ impl ContextVFS<'_> { use_balance, fee_params, )?; + let is_first_message = next_message_is_first(&self.context.data.accumulator.emissions); let call_key = if let Some(method_name) = &calldata.name { abi::CallKey::for_method(method_name) @@ -579,8 +788,8 @@ impl ContextVFS<'_> { my_balance, }, Arc::new(params.clone()), - on, ConsumeInternalArgs { + is_first_message, is_deploy: false, calldata_length, code_length: 0, @@ -630,21 +839,12 @@ impl ContextVFS<'_> { } } - let Some((matched_node, matched_params)) = self - .context - .data - .accumulator - .message_fee_allocation - .iter_mut() - .find_map(|node| { - node.matches_internal( - convert_on_to_modules(on), - address, - convert_call_key_to_modules(call_key), - ) - .map(|params| (node, params)) - }) - else { + let Some((matched_index, matched_params)) = resolve_internal_allocation( + &self.context.data.accumulator.message_fee_allocation, + convert_on_to_modules(on), + address, + convert_call_key_to_modules(call_key), + ) else { log_warn!( recipient = address, call_key:? = call_key, @@ -671,11 +871,9 @@ impl ContextVFS<'_> { let calldata_length = enc.into_inner().0; let fee_params = convert_internal_message_params_to_sdk(matched_params.as_ref()); - let subtree = bytes::Bytes::from( - genvm_modules_interfaces::fees::MessageAllocationNode::abi_encode( - &matched_node.children, - ), - ); + let accumulator = &mut self.context.data.accumulator; + let matched_node = &accumulator.message_fee_allocation[matched_index]; + let subtree = bytes::Bytes::from(matched_node.abi_encode()); let rotations_size = usize_into_u64(fee_params.rotations.len()) .saturating_mul(memory_limiter_consts::MESSAGE_FEE_ROTATION_ELEMENT_SIZE.into()); let allocation = reserve_permanent( @@ -690,10 +888,13 @@ impl ContextVFS<'_> { let fees = consume_message_fee_internal( &self.context.data.supervisor.shared_data, - FeeFunding::Allocation(matched_node), + FeeFunding::Allocation { + node: matched_node, + consumed: &mut accumulator.message_fee_allocation_consumed[matched_index], + }, Arc::new(fee_params.clone()), - on, ConsumeInternalArgs { + is_first_message, is_deploy: false, calldata_length, code_length: 0, @@ -765,6 +966,7 @@ impl ContextVFS<'_> { use_balance, fee_params, )?; + let is_first_message = next_message_is_first(&self.context.data.accumulator.emissions); if let Some(params) = balance_params { let code_length = code.len().into_int_comptime(); @@ -794,8 +996,8 @@ impl ContextVFS<'_> { my_balance, }, Arc::new(params.clone()), - on, ConsumeInternalArgs { + is_first_message, is_deploy: true, calldata_length, code_length, @@ -844,21 +1046,12 @@ impl ContextVFS<'_> { } } - let Some((matched_node, matched_params)) = self - .context - .data - .accumulator - .message_fee_allocation - .iter_mut() - .find_map(|node| { - node.matches_internal( - convert_on_to_modules(on), - calldata::Address::zero(), - convert_call_key_to_modules(abi::CallKey::DEPLOY), - ) - .map(|params| (node, params)) - }) - else { + let Some((matched_index, matched_params)) = resolve_internal_allocation( + &self.context.data.accumulator.message_fee_allocation, + convert_on_to_modules(on), + calldata::Address::zero(), + convert_call_key_to_modules(abi::CallKey::DEPLOY), + ) else { log_warn!( recipient = calldata::Address::zero(), call_key:? = abi::CallKey::DEPLOY, @@ -879,11 +1072,9 @@ impl ContextVFS<'_> { let calldata_length = enc.into_inner().0; let fee_params = convert_internal_message_params_to_sdk(matched_params.as_ref()); - let subtree = bytes::Bytes::from( - genvm_modules_interfaces::fees::MessageAllocationNode::abi_encode( - &matched_node.children, - ), - ); + let accumulator = &mut self.context.data.accumulator; + let matched_node = &accumulator.message_fee_allocation[matched_index]; + let subtree = bytes::Bytes::from(matched_node.abi_encode()); let rotations_size = usize_into_u64(fee_params.rotations.len()) .saturating_mul(memory_limiter_consts::MESSAGE_FEE_ROTATION_ELEMENT_SIZE.into()); let allocation = reserve_permanent( @@ -899,10 +1090,13 @@ impl ContextVFS<'_> { let fees = consume_message_fee_internal( &self.context.data.supervisor.shared_data, - FeeFunding::Allocation(matched_node), + FeeFunding::Allocation { + node: matched_node, + consumed: &mut accumulator.message_fee_allocation_consumed[matched_index], + }, Arc::new(fee_params.clone()), - on, ConsumeInternalArgs { + is_first_message, is_deploy: true, calldata_length, code_length, diff --git a/executor/src/wasi/genlayer_sdk/mod.rs b/executor/src/wasi/genlayer_sdk/mod.rs index 87180f16..d69919a4 100644 --- a/executor/src/wasi/genlayer_sdk/mod.rs +++ b/executor/src/wasi/genlayer_sdk/mod.rs @@ -145,6 +145,7 @@ pub struct VMDataAccumulator { pub messages_value_decremented: primitive_types::U256, pub emissions: Vec, pub message_fee_allocation: Vec, + pub message_fee_allocation_consumed: Vec, } impl VMDataAccumulator { diff --git a/executor/src/wasi/genlayer_sdk/run.rs b/executor/src/wasi/genlayer_sdk/run.rs index 8cdb1816..7c475bf5 100644 --- a/executor/src/wasi/genlayer_sdk/run.rs +++ b/executor/src/wasi/genlayer_sdk/run.rs @@ -438,6 +438,7 @@ impl ContextVFS<'_> { messages_value_decremented: self.context.data.accumulator.messages_value_decremented, emissions: Vec::new(), message_fee_allocation: Vec::new(), + message_fee_allocation_consumed: Vec::new(), }; let vm_data = Box::new(SingleVMData { @@ -539,6 +540,7 @@ impl ContextVFS<'_> { messages_value_decremented: primitive_types::U256::zero(), emissions: Vec::new(), message_fee_allocation: Vec::new(), + message_fee_allocation_consumed: Vec::new(), }, det_subvm_hashes: Default::default(), // A CallContract child is granted the caller's full custom set; @@ -791,11 +793,16 @@ impl ContextVFS<'_> { ))); } - let is_leader = self.context.data.supervisor.shared_data.run_mode == rt::RunMode::Leader; + let run_mode = self.context.data.supervisor.shared_data.run_mode; + let is_leader = run_mode == rt::RunMode::Leader; + let is_validator = run_mode == rt::RunMode::Validator; let mut child_resources = Some((child_topmost_id, child_custom)); // The child gets the caller's budget before this block's output charge. // The snapshot also keeps queued validator work independent of its parent. let mut child_limiter = Some(self.context.limiter.derived()); + // Every non-leader mode re-checks an accepted proposal after caps; only + // a validator additionally puts it to the contract's principle. + let mut accepted_leader_proposal = false; let mut validator_proposal = None; let output = if is_leader { @@ -852,20 +859,16 @@ impl ContextVFS<'_> { match &proposal { // Rejecting is already the disagreement; putting it to the // contract's principle would let a `True` vote it away - LeaderProposal::Rejected(_) - if self.context.data.supervisor.shared_data.run_mode - == rt::RunMode::Validator => - { + LeaderProposal::Rejected(_) if is_validator => { rt::supervisor::mark_nondet_disagreement(&self.context.data.supervisor, call_no) } LeaderProposal::Rejected(_) => {} - LeaderProposal::Accepted(leaders_res) - if self.context.data.supervisor.shared_data.run_mode - == rt::RunMode::Validator => - { - validator_proposal = Some(leaders_res.duplicate()); + LeaderProposal::Accepted(leaders_res) => { + accepted_leader_proposal = true; + if is_validator { + validator_proposal = Some(leaders_res.duplicate()); + } } - LeaderProposal::Accepted(_) => {} } let (result, encoded) = proposal.into_result_and_encoding(); @@ -882,16 +885,23 @@ impl ContextVFS<'_> { ) .await?; - if let Some(leaders_res) = validator_proposal { + if accepted_leader_proposal { if let Err(error) = validate_leader_output_after_caps(&output.encoded, &leader_proposed_encoding) { - rt::supervisor::mark_nondet_disagreement(&self.context.data.supervisor, call_no); + if is_validator { + rt::supervisor::mark_nondet_disagreement( + &self.context.data.supervisor, + call_no, + ); + } return Err(generated::types::Error::trap(crate::anyhow_to_wasmtime( rt::errors::Error::fatal_vm(error).into(), ))); } + } + if let Some(leaders_res) = validator_proposal { let (child_topmost_id, child_custom) = child_resources .take() .expect("nondeterministic child resources are available"); @@ -987,6 +997,7 @@ impl ContextVFS<'_> { messages_value_decremented: primitive_types::U256::max_value(), emissions: Vec::new(), message_fee_allocation: Vec::new(), + message_fee_allocation_consumed: Vec::new(), }; std::mem::swap(&mut self.context.data.accumulator, &mut fake_my_data); diff --git a/executor/src/wasi/genlayer_sdk/tests.rs b/executor/src/wasi/genlayer_sdk/tests.rs index ff18f3bd..37d535f7 100644 --- a/executor/src/wasi/genlayer_sdk/tests.rs +++ b/executor/src/wasi/genlayer_sdk/tests.rs @@ -1,4 +1,7 @@ -use super::message::{validate_balance_fee, FEE_PARAM_COUNT_BITS, FEE_PARAM_PRICE_BITS}; +use super::message::{ + external_allocation_candidates, next_message_is_first, resolve_internal_allocation, + validate_balance_fee, FEE_PARAM_COUNT_BITS, FEE_PARAM_PRICE_BITS, +}; use super::run::{ call_contract_route, charge_nondet_output, derive_call_contract_permissions, leader_outcome_for_publication, leader_proposal_for_validation, nested_run_ok, @@ -28,12 +31,12 @@ fn errno(e: generated::types::Error) -> generated::types::Errno { fn nondet_fees_with_delta(total: u64, nondet_delta: &str) -> rt::fees::DataLimit { let bucket = |delta: &str| crate::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".to_owned(), delta_expr: delta.to_owned(), }; rt::fees::DataLimit::new( - vec![U256::from(total)], + std::collections::HashMap::from([("test".to_owned(), U256::from(total))]), crate::config::FeesConfig { expr_prelude: String::new(), storage: bucket("\\attrs = 0"), @@ -53,7 +56,7 @@ fn nondet_fees(total: u64) -> rt::fees::DataLimit { fn emission_fees() -> crate::config::FeesConfig { let bucket = |delta: &str| crate::config::FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".to_owned(), delta_expr: delta.to_owned(), }; @@ -104,6 +107,143 @@ fn internal_message_allocation() -> genvm_modules_interfaces::fees::MessageAlloc } } +fn allocation_child( + budget: U256, + children: Vec, +) -> genvm_modules_interfaces::fees::MessageAllocationNode { + let mut node = internal_message_allocation(); + node.budget = budget; + node.children = children; + node +} + +#[test] +fn internal_allocation_prefers_exact_key_over_earlier_wildcard() { + 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); + wildcard.budget = U256::one(); + let mut exact = wildcard.clone(); + exact.call_key = Some(call_key); + exact.budget = U256::from(2); + let nodes = vec![wildcard, exact]; + + let (matched, _) = resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .expect("exact allocation should match"); + + assert_eq!(nodes[matched].budget, U256::from(2)); +} + +#[test] +fn internal_allocation_skips_zero_budget_exact_key() { + 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); + wildcard.budget = U256::one(); + let mut exact = wildcard.clone(); + exact.call_key = Some(call_key); + exact.budget = U256::zero(); + let nodes = vec![wildcard, exact]; + + let (matched, _) = resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .expect("wildcard allocation should match"); + + assert_eq!(nodes[matched].budget, U256::one()); +} + +#[test] +fn internal_allocation_phase_is_checked_after_key_resolution() { + 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; + let nodes = vec![wildcard, exact]; + + assert!(resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Finalized, + recipient, + call_key, + ) + .is_none()); +} + +#[test] +fn internal_allocation_selects_phase_within_equal_keys() { + let recipient = calldata::Address::from([7; 20]); + let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); + let mut finalized = internal_message_allocation(); + finalized.budget = U256::one(); + let mut decided = finalized.clone(); + decided.on = genvm_modules_interfaces::On::Decided; + decided.budget = U256::from(2); + let nodes = vec![finalized, decided]; + + let (matched, _) = resolve_internal_allocation( + &nodes, + genvm_modules_interfaces::On::Decided, + recipient, + call_key, + ) + .expect("decided allocation should match"); + + assert_eq!(nodes[matched].budget, U256::from(2)); +} + +#[test] +fn external_allocation_candidates_follow_consensus_precedence() { + let recipient = calldata::Address::from([7; 20]); + let call_key = genvm_modules_interfaces::abi_stub::CallKey([8; 32]); + let mut global_wildcard = external_message_allocation(); + global_wildcard.budget = U256::one(); + let mut recipient_wildcard = global_wildcard.clone(); + recipient_wildcard.recipient = Some(recipient); + recipient_wildcard.budget = U256::from(2); + let mut exact = recipient_wildcard.clone(); + exact.call_key = Some(call_key); + exact.budget = U256::from(3); + let nodes = vec![global_wildcard, recipient_wildcard, exact]; + + let candidates = external_allocation_candidates(&nodes, recipient, call_key); + let budgets = candidates + .into_iter() + .map(|index| nodes[index].budget) + .collect::>(); + + assert_eq!(budgets, vec![U256::from(3), U256::from(2), U256::one()]); +} + +#[test] +fn external_allocation_candidates_include_zero_budget_nodes() { + 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 = U256::zero(); + let nodes = vec![node]; + + assert_eq!( + external_allocation_candidates(&nodes, recipient, call_key), + vec![0] + ); +} + struct TestDir(std::path::PathBuf); impl TestDir { @@ -203,7 +343,7 @@ impl EmissionTestContext { ..Default::default() }, data_fees_limit: rt::fees::DataLimit::new( - vec![U256::from(fee_total)], + std::collections::HashMap::from([("test".to_owned(), U256::from(fee_total))]), fees.clone(), Default::default(), ) @@ -343,6 +483,7 @@ impl EmissionTestContext { external_message_allocation(), internal_message_allocation(), ], + message_fee_allocation_consumed: vec![U256::zero(); 2], }, det_subvm_hashes: Default::default(), granted_custom: Vec::new(), @@ -450,6 +591,29 @@ fn emission_allocation_overflow_cannot_fit_the_budget() { assert_eq!(emission_allocation_size(&[u64::MAX]), u64::MAX); } +#[test] +fn first_message_flag_ignores_events_and_flips_after_a_message() { + let event = domain::ExecutionEmission::Event { + topics: Vec::new(), + blob: calldata::Map::new().into(), + storage_fee: U256::zero(), + }; + assert!(next_message_is_first(&[event])); + + let message = domain::ExecutionEmission::ExternalMessage { + address: calldata::Address::zero(), + calldata: bytes::Bytes::new(), + value: U256::zero(), + message_fee: U256::zero(), + receipt_fee: U256::zero(), + fee_params: abi::fees::ExternalMessageParams { + gas_limit: U256::zero(), + max_gas_price: U256::zero(), + }, + }; + assert!(!next_message_is_first(&[message])); +} + #[tokio::test] async fn messages_rejected_by_memory_are_not_appended_or_charged() { for emission in MessageEmission::ALL { @@ -476,7 +640,7 @@ async fn messages_rejected_by_memory_are_not_appended_or_charged() { .data_fees_limit .remaining() .await, - vec![U256::from(1)], + std::collections::BTreeMap::from([("test".to_owned(), U256::from(1))]), "{} was charged", emission.name() ); @@ -567,6 +731,186 @@ async fn messages_rejected_by_fee_are_not_appended_and_release_memory() { } } +#[tokio::test] +async fn unallocated_external_receipt_exhaustion_is_classified_as_receipt() { + let mut test = EmissionTestContext::new(u32::MAX, 0); + test.context + .data + .accumulator + .message_fee_allocation + .retain(|node| { + matches!( + &node.fee_params, + genvm_modules_interfaces::fees::MessageAllocationNodeParams::Internal(_) + ) + }); + + let error = test + .emit_message(MessageEmission::External) + .await + .unwrap_err(); + + assert!(trap_message(error).contains("out_of receipt message")); + test.shutdown().await; +} + +#[tokio::test] +async fn repeated_internal_messages_preserve_canonical_subtree_and_charge_budgets() { + for emission in [ + MessageEmission::InternalAllocation, + MessageEmission::DeployAllocation, + ] { + let mut test = EmissionTestContext::new(u32::MAX, 14); + let grandchild = allocation_child(U256::one(), Vec::new()); + let first_child = allocation_child(U256::from(2), vec![grandchild]); + let second_child = allocation_child(U256::from(3), Vec::new()); + test.context.data.accumulator.message_fee_allocation[1].children = + vec![first_child, second_child]; + + test.emit_message(emission).await.unwrap(); + test.emit_message(emission).await.unwrap(); + + let emission_data = |emission: &domain::ExecutionEmission| match emission { + domain::ExecutionEmission::InternalMessage { + message_fee, + subtree, + .. + } + | domain::ExecutionEmission::InternalDeployMessage { + message_fee, + subtree, + .. + } => (*message_fee, subtree.clone()), + other => panic!("unexpected emission: {other:?}"), + }; + let first = emission_data(&test.context.data.accumulator.emissions[0]); + let second = emission_data(&test.context.data.accumulator.emissions[1]); + assert_eq!(first.0, U256::from(6), "{}", emission.name()); + assert_eq!(second.0, U256::from(6), "{}", emission.name()); + assert_eq!(first.1, second.1, "{} subtree changed", emission.name()); + assert_eq!( + test.context.data.accumulator.message_fee_allocation[1].budget, + U256::from(100), + "{}", + emission.name() + ); + assert_eq!( + test.context + .data + .accumulator + .message_fee_allocation_consumed[1], + U256::from(12), + "{}", + emission.name() + ); + let consumed = test + .context + .data + .supervisor + .shared_data + .data_fees_limit + .consumed() + .await; + assert_eq!(consumed.message_fee, U256::from(12), "{}", emission.name()); + assert_eq!( + consumed.message_receipt, + U256::from(2), + "{}", + emission.name() + ); + + test.shutdown().await; + } +} + +#[tokio::test] +async fn child_budget_fee_failure_is_atomic() { + let mut test = EmissionTestContext::new(u32::MAX, 6); + test.context.data.accumulator.message_fee_allocation[1].children = vec![ + allocation_child(U256::from(2), Vec::new()), + allocation_child(U256::from(3), Vec::new()), + ]; + let memory_before = test.context.limiter.get_remaining_memory(); + + let error = test + .emit_message(MessageEmission::InternalAllocation) + .await + .unwrap_err(); + + assert!( + trap_message(error).contains("out_of message_fee total # internal"), + "unexpected fee error" + ); + assert!(test.context.data.accumulator.emissions.is_empty()); + assert_eq!( + test.context.data.accumulator.message_fee_allocation[1].budget, + U256::from(100) + ); + assert_eq!( + test.context + .data + .accumulator + .message_fee_allocation_consumed[1], + U256::zero() + ); + assert_eq!(test.context.limiter.get_remaining_memory(), memory_before); + let consumed = test + .context + .data + .supervisor + .shared_data + .data_fees_limit + .consumed() + .await; + assert_eq!(consumed.message_fee, U256::zero()); + assert_eq!(consumed.message_receipt, U256::zero()); + + test.shutdown().await; +} + +#[tokio::test] +async fn child_budget_overflow_is_internal_and_has_no_effect() { + let mut test = EmissionTestContext::new(u32::MAX, 1); + test.context.data.accumulator.message_fee_allocation[1].children = + vec![allocation_child(U256::MAX, Vec::new())]; + let memory_before = test.context.limiter.get_remaining_memory(); + + let error = test + .emit_message(MessageEmission::InternalAllocation) + .await + .unwrap_err(); + + assert!( + trap_message(error).contains("message declared budget overflow"), + "unexpected overflow error" + ); + assert!(test.context.data.accumulator.emissions.is_empty()); + assert_eq!( + test.context.data.accumulator.message_fee_allocation[1].budget, + U256::from(100) + ); + assert_eq!( + test.context + .data + .accumulator + .message_fee_allocation_consumed[1], + U256::zero() + ); + assert_eq!(test.context.limiter.get_remaining_memory(), memory_before); + let consumed = test + .context + .data + .supervisor + .shared_data + .data_fees_limit + .consumed() + .await; + assert_eq!(consumed.message_fee, U256::zero()); + assert_eq!(consumed.message_receipt, U256::zero()); + + test.shutdown().await; +} + #[tokio::test] async fn event_rejected_by_memory_is_not_appended_or_charged() { let mut test = EmissionTestContext::new(0, 1); @@ -591,7 +935,7 @@ async fn event_rejected_by_memory_is_not_appended_or_charged() { .data_fees_limit .remaining() .await, - vec![U256::from(1)] + std::collections::BTreeMap::from([("test".to_owned(), U256::from(1))]) ); test.shutdown().await; @@ -705,7 +1049,10 @@ async fn nondet_fee_preflight_fails_before_consuming_the_fallback_fee() { .await .is_err() ); - assert_eq!(fees.remaining().await, vec![U256::from(required - 1)]); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), U256::from(required - 1))]) + ); assert_eq!(fees.consumed().await.nondet_output, U256::zero()); } @@ -751,7 +1098,10 @@ async fn over_cap_nondet_payload_is_replaced_before_publication() { limiter.get_remaining_memory(), memory_budget - fee_error.allocation_size() as u32 ); - assert_eq!(fees.remaining().await, vec![U256::zero()]); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), U256::zero())]) + ); assert_eq!( fees.consumed().await.nondet_output, U256::from(fee_error_len) @@ -878,8 +1228,8 @@ fn zero_price_caps_are_inval() { #[test] fn huge_magnitude_params_are_inval() { // Security-review N1 repro: passes the emptiness/zero checks, but the - // 2^250 magnitudes would push messageFeeFloor past U256 and trip the - // evaluator's internal `fee cost exceeds U256 range` abort. + // 2^250 magnitudes would push messageFeeFloor past U256 and saturate the + // evaluator's result to U256::MAX. let p = abi::fees::InternalMessageParams { leader_time_units_allocation: U256::one() << 250, validator_time_units_allocation: U256::zero(), diff --git a/executor/tests/code_and_major_reads.rs b/executor/tests/code_and_major_reads.rs index 7592b919..a0b05ad9 100644 --- a/executor/tests/code_and_major_reads.rs +++ b/executor/tests/code_and_major_reads.rs @@ -38,7 +38,7 @@ impl HostStorageLocking for FakeHost { /// A minimal `DataLimit` whose storage bucket charges one unit per page. fn data_fees(total_pages: u64) -> Limiter { let bucket = |delta: &str| FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".to_owned(), delta_expr: delta.to_owned(), }; @@ -51,7 +51,10 @@ fn data_fees(total_pages: u64) -> Limiter { event: bucket("\\attrs = 0"), }; let dl = rt::fees::DataLimit::new( - vec![primitive_types::U256::from(total_pages)], + std::collections::HashMap::from([( + "test".to_owned(), + primitive_types::U256::from(total_pages), + )]), fees, Default::default(), ) diff --git a/executor/tests/fee_bucket_accounting.rs b/executor/tests/fee_bucket_accounting.rs new file mode 100644 index 00000000..aacb9a9e --- /dev/null +++ b/executor/tests/fee_bucket_accounting.rs @@ -0,0 +1,67 @@ +use genvm::config::{FeesBucketConfig, FeesConfig}; +use genvm::rt::fees::{CostVec, DataLimit}; +use primitive_types::U256; + +fn config(event: FeesBucketConfig) -> FeesConfig { + let bucket = || FeesBucketConfig { + buckets: vec![symbol_table::GlobalSymbol::from("test")], + subtract_on_start_expr: "0".to_owned(), + delta_expr: "\\attrs = 0".to_owned(), + }; + FeesConfig { + expr_prelude: String::new(), + storage: bucket(), + message_receipt: bucket(), + nondet_output: bucket(), + message_fee: bucket(), + event, + } +} + +fn data_limit(fees: FeesConfig) -> DataLimit { + DataLimit::new( + std::collections::HashMap::from([("test".to_owned(), U256::MAX)]), + fees, + Default::default(), + ) + .unwrap() +} + +#[tokio::test] +async fn duplicate_bucket_cost_overflow_is_rejected_atomically() { + let event = FeesBucketConfig { + buckets: vec![ + symbol_table::GlobalSymbol::from("test"), + symbol_table::GlobalSymbol::from("test"), + ], + subtract_on_start_expr: "0".to_owned(), + delta_expr: format!("\\attrs = [{}, 1]", U256::MAX), + }; + let fees = data_limit(config(event)); + + assert_eq!(fees.consume_event(0, 0).await.unwrap(), None); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), U256::MAX)]) + ); +} + +#[tokio::test] +async fn shared_message_bucket_cost_overflow_is_rejected_atomically() { + let event = FeesBucketConfig { + buckets: vec![symbol_table::GlobalSymbol::from("test")], + subtract_on_start_expr: "0".to_owned(), + delta_expr: "\\attrs = 0".to_owned(), + }; + let fees = data_limit(config(event)); + + assert!( + !fees + .consume_message_fee(&CostVec(vec![U256::MAX]), &CostVec(vec![U256::one()])) + .await + ); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), U256::MAX)]) + ); +} diff --git a/executor/tests/fee_bucket_config.rs b/executor/tests/fee_bucket_config.rs new file mode 100644 index 00000000..929a9212 --- /dev/null +++ b/executor/tests/fee_bucket_config.rs @@ -0,0 +1,71 @@ +use genvm::config::FeesBucketConfig; + +fn parse(input: &str) -> Result { + serde_yaml::from_str(input) +} + +#[test] +fn bucket_config_accepts_one_named_bucket() { + let config = parse("buckets: execution_data_gas\ndelta_expr: '\\a = 0'").unwrap(); + + assert_eq!(config.buckets.len(), 1); + assert_eq!(config.buckets[0].as_str(), "execution_data_gas"); +} + +#[test] +fn bucket_config_accepts_multiple_named_buckets() { + let config = + parse("buckets: [execution_data_gas, submitted_messages]\ndelta_expr: '\\a = 0'").unwrap(); + + let names = config + .buckets + .iter() + .map(symbol_table::GlobalSymbol::as_str) + .collect::>(); + assert_eq!(names, ["execution_data_gas", "submitted_messages"]); +} + +#[test] +fn bucket_config_rejects_numeric_buckets() { + let error = parse("buckets: 0\ndelta_expr: '\\a = 0'").unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("non-empty string"), + "unexpected error: {message}" + ); +} + +#[test] +fn bucket_config_rejects_empty_names() { + let error = parse("buckets: ''\ndelta_expr: '\\a = 0'").unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("must not be empty"), + "unexpected error: {message}" + ); +} + +#[test] +fn bucket_config_rejects_an_empty_list() { + let error = parse("buckets: []\ndelta_expr: '\\a = 0'").unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("at least one entry"), + "unexpected error: {message}" + ); +} + +#[test] +fn bucket_config_rejects_legacy_bucket_number() { + let error = + parse("buckets: execution_data_gas\nbucket_no: 0\ndelta_expr: '\\a = 0'").unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("unknown field `bucket_no`"), + "unexpected error: {message}" + ); +} diff --git a/executor/tests/message_fee_external.rs b/executor/tests/message_fee_external.rs new file mode 100644 index 00000000..57497232 --- /dev/null +++ b/executor/tests/message_fee_external.rs @@ -0,0 +1,76 @@ +use genvm::config::FeesConfig; +use genvm::rt::fees::DataLimit; +use primitive_types::U256; + +fn default_fees() -> FeesConfig { + let config: serde_yaml::Value = + serde_yaml::from_str(include_str!("../install/config/genvm.yaml")).unwrap(); + serde_yaml::from_value(config["fees"].clone()).unwrap() +} + +fn bucket_totals() -> std::collections::HashMap { + [ + "execution_data_gas", + "message_fee", + "nondet_outputs", + "submitted_messages", + "submitted_messages_count", + ] + .into_iter() + .map(|name| (name.to_owned(), U256::MAX)) + .collect() +} + +/// Deliberately omits every constant only the internal branch reads +/// (`overlaySplitBps`, the phase-timeout bounds): the external branch must not +/// force those bindings. +fn gas_data(locked_receipt_gas_price: u64) -> std::collections::BTreeMap { + [ + ("storageUnitPrice", "1".to_owned()), + ( + "lockedReceiptGasPrice", + locked_receipt_gas_price.to_string(), + ), + ("receiptGasPerByte", "1".to_owned()), + ("gasPerChangedSlot", "1".to_owned()), + ("intrinsicGas", "0".to_owned()), + ("bootloaderOverhead", "0".to_owned()), + ("fixedProposeReceiptGas", "0".to_owned()), + ("fixedMessageRevealGas", "0".to_owned()), + ("receiptWrapperBytes", "1024".to_owned()), + ] + .into_iter() + .map(|(name, value)| (name.to_owned(), value)) + .collect() +} + +fn fee(locked_receipt_gas_price: u64, gas_limit: u64, max_gas_price: u64) -> U256 { + let fees = DataLimit::new( + bucket_totals(), + default_fees(), + gas_data(locked_receipt_gas_price), + ) + .unwrap(); + + fees.calculate_message_fee_external(&genlayer_sdk::abi::fees::ExternalMessageParams { + gas_limit: gas_limit.into(), + max_gas_price: max_gas_price.into(), + }) + .unwrap() + .reported_fee() +} + +#[test] +fn external_fee_uses_the_locked_price_when_it_is_lower() { + assert_eq!(fee(3, 1000, 7), U256::from(3000)); +} + +#[test] +fn external_fee_uses_the_guest_cap_when_it_is_lower() { + assert_eq!(fee(7, 1000, 3), U256::from(3000)); +} + +#[test] +fn external_fee_is_price_agnostic_when_both_agree() { + assert_eq!(fee(5, 1000, 5), U256::from(5000)); +} diff --git a/executor/tests/message_fee_overlay.rs b/executor/tests/message_fee_overlay.rs new file mode 100644 index 00000000..721dd9de --- /dev/null +++ b/executor/tests/message_fee_overlay.rs @@ -0,0 +1,95 @@ +use genvm::config::FeesConfig; +use genvm::rt::fees::DataLimit; +use primitive_types::U256; + +fn default_fees() -> FeesConfig { + let config: serde_yaml::Value = + serde_yaml::from_str(include_str!("../install/config/genvm.yaml")).unwrap(); + serde_yaml::from_value(config["fees"].clone()).unwrap() +} + +fn bucket_totals() -> std::collections::HashMap { + [ + "execution_data_gas", + "message_fee", + "nondet_outputs", + "submitted_messages", + "submitted_messages_count", + ] + .into_iter() + .map(|name| (name.to_owned(), U256::MAX)) + .collect() +} + +fn gas_data_without_overlay() -> std::collections::BTreeMap { + [ + ("storageUnitPrice", "1"), + ("lockedReceiptGasPrice", "1"), + ("receiptGasPerByte", "1"), + ("gasPerChangedSlot", "1"), + ("intrinsicGas", "0"), + ("bootloaderOverhead", "0"), + ("fixedProposeReceiptGas", "0"), + ("fixedMessageRevealGas", "0"), + ("receiptWrapperBytes", "1024"), + ("minProposeTimeout", "1"), + ( + "maxProposeTimeout", + "340282366920938463463374607431768211455", + ), + ("minCommitTimeout", "1"), + ( + "maxCommitTimeout", + "340282366920938463463374607431768211455", + ), + ] + .into_iter() + .map(|(name, value)| (name.to_owned(), value.to_owned())) + .collect() +} + +fn fee_params() -> genlayer_sdk::abi::fees::InternalMessageParams { + genlayer_sdk::abi::fees::InternalMessageParams { + leader_time_units_allocation: U256::one(), + validator_time_units_allocation: U256::one(), + execution_budget_per_round: U256::one(), + rotations: vec![U256::zero()], + max_price_gen_per_time_unit: U256::one(), + storage_fee_max_gas_price: U256::one(), + receipt_fee_max_gas_price: U256::one(), + } +} + +#[test] +fn missing_overlay_split_is_not_treated_as_zero() { + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data_without_overlay()).unwrap(); + + let error = fees + .calculate_message_fee_internal(&fee_params()) + .unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("overlaySplitBps"), + "unexpected error: {message}" + ); +} + +#[test] +fn overlay_split_at_or_above_full_share_is_rejected() { + for bps in ["10000", "12000"] { + let mut gas_data = gas_data_without_overlay(); + gas_data.insert("overlaySplitBps".to_owned(), bps.to_owned()); + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data).unwrap(); + + let error = fees + .calculate_message_fee_internal(&fee_params()) + .unwrap_err(); + let message = error.to_string(); + + assert!( + message.contains("overlaySplitBps must be below 10000"), + "unexpected error for {bps}: {message}" + ); + } +} diff --git a/executor/tests/message_fee_time_units.rs b/executor/tests/message_fee_time_units.rs new file mode 100644 index 00000000..5eb23e0b --- /dev/null +++ b/executor/tests/message_fee_time_units.rs @@ -0,0 +1,116 @@ +use genvm::config::FeesConfig; +use genvm::rt::fees::DataLimit; +use primitive_types::U256; + +fn default_fees() -> FeesConfig { + let config: serde_yaml::Value = + serde_yaml::from_str(include_str!("../install/config/genvm.yaml")).unwrap(); + serde_yaml::from_value(config["fees"].clone()).unwrap() +} + +fn bucket_totals() -> std::collections::HashMap { + [ + "execution_data_gas", + "message_fee", + "nondet_outputs", + "submitted_messages", + "submitted_messages_count", + ] + .into_iter() + .map(|name| (name.to_owned(), U256::MAX)) + .collect() +} + +fn gas_data( + min_propose: u64, + max_propose: u64, + min_commit: u64, + max_commit: u64, +) -> std::collections::BTreeMap { + [ + ("storageUnitPrice", "1".to_owned()), + ("lockedReceiptGasPrice", "1".to_owned()), + ("receiptGasPerByte", "1".to_owned()), + ("gasPerChangedSlot", "1".to_owned()), + ("intrinsicGas", "0".to_owned()), + ("bootloaderOverhead", "0".to_owned()), + ("fixedProposeReceiptGas", "0".to_owned()), + ("fixedMessageRevealGas", "0".to_owned()), + ("overlaySplitBps", "0".to_owned()), + ("receiptWrapperBytes", "1024".to_owned()), + ("minProposeTimeout", min_propose.to_string()), + ("maxProposeTimeout", max_propose.to_string()), + ("minCommitTimeout", min_commit.to_string()), + ("maxCommitTimeout", max_commit.to_string()), + ] + .into_iter() + .map(|(name, value)| (name.to_owned(), value)) + .collect() +} + +fn fee_params( + leader_time_units: u64, + validator_time_units: u64, +) -> genlayer_sdk::abi::fees::InternalMessageParams { + genlayer_sdk::abi::fees::InternalMessageParams { + leader_time_units_allocation: leader_time_units.into(), + validator_time_units_allocation: validator_time_units.into(), + execution_budget_per_round: U256::one(), + rotations: vec![U256::zero()], + max_price_gen_per_time_unit: U256::one(), + storage_fee_max_gas_price: U256::one(), + receipt_fee_max_gas_price: U256::one(), + } +} + +#[test] +fn both_zero_disables_phase_timeout_validation() { + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data(5, 10, 20, 30)).unwrap(); + + fees.calculate_message_fee_internal(&fee_params(0, 0)) + .unwrap(); +} + +#[test] +fn phase_specific_bounds_are_inclusive() { + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data(5, 5, 10, 10)).unwrap(); + + fees.calculate_message_fee_internal(&fee_params(5, 10)) + .unwrap(); +} + +#[test] +fn nonzero_phase_timeouts_outside_bounds_are_rejected() { + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data(5, 10, 20, 30)).unwrap(); + + for (leader, validator) in [(4, 20), (11, 20), (5, 19), (5, 31), (0, 20), (5, 0)] { + let error = fees + .calculate_message_fee_internal(&fee_params(leader, validator)) + .unwrap_err(); + let message = error.to_string(); + assert!( + message.contains("fee below_minimum"), + "unexpected error for leader={leader}, validator={validator}: {message}" + ); + } +} + +#[test] +fn primary_fee_is_not_multiplied_by_appeal_lifecycle() { + let mut gas_data = gas_data(1, u64::MAX, 1, u64::MAX); + gas_data.insert("overlaySplitBps".to_owned(), "1500".to_owned()); + let fees = DataLimit::new(bucket_totals(), default_fees(), gas_data).unwrap(); + let params = genlayer_sdk::abi::fees::InternalMessageParams { + leader_time_units_allocation: U256::from(5), + validator_time_units_allocation: U256::from(5), + execution_budget_per_round: U256::from(1024), + rotations: vec![U256::from(4); 5], + max_price_gen_per_time_unit: U256::from(3), + storage_fee_max_gas_price: U256::from(20), + receipt_fee_max_gas_price: U256::from(20), + }; + + let fee = fees.calculate_message_fee_internal(¶ms).unwrap(); + + assert_eq!(fee.reported_fee(), U256::from(47_837)); +} diff --git a/executor/tests/message_receipt_fees.rs b/executor/tests/message_receipt_fees.rs new file mode 100644 index 00000000..5290087a --- /dev/null +++ b/executor/tests/message_receipt_fees.rs @@ -0,0 +1,91 @@ +use genvm::config::FeesConfig; +use genvm::rt::fees::{DataLimit, MessageReceiptParams}; +use primitive_types::U256; + +fn default_fees() -> FeesConfig { + let config: genvm::config::Config = + serde_yaml::from_str(include_str!("../install/config/genvm.yaml")).unwrap(); + config.fees +} + +fn gas_data() -> std::collections::BTreeMap { + [ + ("bootloaderOverhead", 60_000), + ("fixedMessageRevealGas", 100_000), + ("fixedProposeReceiptGas", 210_000), + ("gasPerChangedSlot", 1_000), + ("intrinsicGas", 21_000), + ("receiptGasPerByte", 16), + ("receiptWrapperBytes", 1_024), + ] + .map(|(name, value)| (name.to_owned(), value.to_string())) + .into() +} + +fn data_limit( + execution_data_gas: u64, + submitted_messages: u64, + submitted_messages_count: u64, +) -> DataLimit { + DataLimit::new( + std::collections::HashMap::from([ + ( + "execution_data_gas".to_owned(), + U256::from(execution_data_gas), + ), + ("message_fee".to_owned(), U256::zero()), + ("nondet_outputs".to_owned(), U256::from(64)), + ( + "submitted_messages".to_owned(), + U256::from(submitted_messages), + ), + ( + "submitted_messages_count".to_owned(), + U256::from(submitted_messages_count), + ), + ]), + default_fees(), + gas_data(), + ) + .unwrap() +} + +fn empty_external_message(is_first_message: bool) -> MessageReceiptParams { + MessageReceiptParams { + is_first_message, + is_internal: false, + is_deploy: false, + rotations_count: 0, + calldata_length: 0, + code_length: 0, + subtree_length: 0, + } +} + +#[tokio::test] +async fn message_free_initial_charge_excludes_reveal_cost() { + let fees = data_limit(315_408, 0, 0); + + assert!(fees.consume_initial().await.is_none()); + assert_eq!(fees.remaining().await["execution_data_gas"], U256::zero()); +} + +#[tokio::test] +async fn reveal_cost_is_charged_with_only_the_first_message() { + let fees = data_limit(518_888, 1_280, 2); + assert!(fees.consume_initial().await.is_none()); + let message = fees + .calculate_message_receipt(empty_external_message(true)) + .unwrap(); + let next_message = fees + .calculate_message_receipt(empty_external_message(false)) + .unwrap(); + + assert!(fees.consume_message_receipt_only(&message).await); + assert!(fees.consume_message_receipt_only(&next_message).await); + + let remaining = fees.remaining().await; + assert_eq!(remaining["execution_data_gas"], U256::zero()); + assert_eq!(remaining["submitted_messages"], U256::zero()); + assert_eq!(remaining["submitted_messages_count"], U256::zero()); +} diff --git a/executor/tests/nondet_output_fees.rs b/executor/tests/nondet_output_fees.rs index 7cdfbf04..d5ce8bce 100644 --- a/executor/tests/nondet_output_fees.rs +++ b/executor/tests/nondet_output_fees.rs @@ -3,7 +3,7 @@ use genvm::rt::fees::DataLimit; fn nondet_fees(total: u64) -> DataLimit { let bucket = |delta: &str| FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".to_owned(), delta_expr: delta.to_owned(), }; @@ -16,7 +16,7 @@ fn nondet_fees(total: u64) -> DataLimit { event: bucket("\\attrs = 0"), }; DataLimit::new( - vec![primitive_types::U256::from(total)], + std::collections::HashMap::from([("test".to_owned(), primitive_types::U256::from(total))]), fees, Default::default(), ) @@ -29,7 +29,10 @@ async fn nondet_fee_preflight_checks_without_consuming() { assert!(fees.can_consume_nondet_output(5).await.unwrap()); assert!(!fees.can_consume_nondet_output(6).await.unwrap()); - assert_eq!(fees.remaining().await, vec![primitive_types::U256::from(5)]); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), primitive_types::U256::from(5),)]) + ); assert_eq!( fees.consumed().await.nondet_output, primitive_types::U256::zero() @@ -42,7 +45,10 @@ async fn nondet_fee_preflight_leaves_the_checked_charge_available() { assert!(fees.can_consume_nondet_output(5).await.unwrap()); assert!(fees.consume_nondet_output(5).await.unwrap()); - assert_eq!(fees.remaining().await, vec![primitive_types::U256::zero()]); + assert_eq!( + fees.remaining().await, + std::collections::BTreeMap::from([("test".to_owned(), primitive_types::U256::zero(),)]) + ); assert_eq!( fees.consumed().await.nondet_output, primitive_types::U256::from(5) diff --git a/executor/tests/storage_page_accounting.rs b/executor/tests/storage_page_accounting.rs index 87424be5..402fa214 100644 --- a/executor/tests/storage_page_accounting.rs +++ b/executor/tests/storage_page_accounting.rs @@ -31,7 +31,7 @@ impl HostStorageLocking for FakeHost { /// A minimal `DataLimit` whose storage bucket charges one unit per page. fn data_fees(total_pages: u64) -> Limiter { let bucket = |delta: &str| FeesBucketConfig { - bucket_no: vec![0], + buckets: vec![symbol_table::GlobalSymbol::from("test")], subtract_on_start_expr: "0".to_owned(), delta_expr: delta.to_owned(), }; @@ -44,7 +44,10 @@ fn data_fees(total_pages: u64) -> Limiter { event: bucket("\\attrs = 0"), }; let dl = rt::fees::DataLimit::new( - vec![primitive_types::U256::from(total_pages)], + std::collections::HashMap::from([( + "test".to_owned(), + primitive_types::U256::from(total_pages), + )]), fees, Default::default(), ) diff --git a/tests/integration/balance/balance/balance.0_0.stdout b/tests/integration/balance/balance/balance.0_0.stdout index d0085600..122131f6 100644 --- a/tests/integration/balance/balance/balance.0_0.stdout +++ b/tests/integration/balance/balance/balance.0_0.stdout @@ -7,4 +7,4 @@ main At(self) 10 nested self 10 nested At(self) 10 executed with `Return(null)` -{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":225,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":5} +{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":5} diff --git a/tests/integration/balance/balance_eth/balance_eth.0_0.stdout b/tests/integration/balance/balance_eth/balance_eth.0_0.stdout index a9d5932a..4a0e75cb 100644 --- a/tests/integration/balance/balance_eth/balance_eth.0_0.stdout +++ b/tests/integration/balance/balance_eth/balance_eth.0_0.stdout @@ -7,4 +7,4 @@ main At(self) 10 nested self 10 nested At(self) 10 executed with `Return(null)` -{"address":addr#0200000000000000000000000000000000000000,"calldata":b#,"fee_params":{"gas_limit":1606938044258990275541962092341162602522202993782792835301376,"max_gas_price":0},"message_fee":0,"receipt_fee":129,"type":"ExternalMessage","value":5} +{"address":addr#0200000000000000000000000000000000000000,"calldata":b#,"fee_params":{"gas_limit":1606938044258990275541962092341162602522202993782792835301376,"max_gas_price":0},"message_fee":0,"receipt_fee":673,"type":"ExternalMessage","value":5} diff --git a/tests/integration/balance/sandbox_overspend/sandbox_overspend.0.stdout b/tests/integration/balance/sandbox_overspend/sandbox_overspend.0.stdout index 4dde3633..d3ebd911 100644 --- a/tests/integration/balance/sandbox_overspend/sandbox_overspend.0.stdout +++ b/tests/integration/balance/sandbox_overspend/sandbox_overspend.0.stdout @@ -3,4 +3,4 @@ sandbox transfer failed: 7: insufficient_balance sandbox result=Return(calldata=40) balance final=40 executed with `Return(null)` -{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":225,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":60} +{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":60} diff --git a/tests/integration/balance/sandbox_overspend_2/sandbox_overspend_2.0.stdout b/tests/integration/balance/sandbox_overspend_2/sandbox_overspend_2.0.stdout index f80f8c14..0c5ed332 100644 --- a/tests/integration/balance/sandbox_overspend_2/sandbox_overspend_2.0.stdout +++ b/tests/integration/balance/sandbox_overspend_2/sandbox_overspend_2.0.stdout @@ -3,4 +3,4 @@ sandbox result=Return(calldata=40) balance after sandbox=40 transfer failed with error: 7: insufficient_balance balance final=40 executed with `Return(null)` -{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":225,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":60} +{"address":addr#0200000000000000000000000000000000000000,"call_key":b#0000000000000000000000000000000000000000000000000000000000000000,"calldata":{},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":60} diff --git a/tests/integration/exploit/storage_distinct_pages/storage_distinct_pages.jsonnet b/tests/integration/exploit/storage_distinct_pages/storage_distinct_pages.jsonnet index 950b7bd7..ebeb031d 100644 --- a/tests/integration/exploit/storage_distinct_pages/storage_distinct_pages.jsonnet +++ b/tests/integration/exploit/storage_distinct_pages/storage_distinct_pages.jsonnet @@ -1,7 +1,7 @@ local msg = import 'templates/message.json'; local util = import 'templates/util.jsonnet'; -// bucket 0 is shared with the receipt buckets, so zero the receipt prices out to +// execution_data_gas is shared with the receipt buckets, so zero their prices to // leave changed pages as its only consumer local storageOnlyGasData = { storageUnitPrice: '1', @@ -11,16 +11,17 @@ local storageOnlyGasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', }; -// bucket 0 = storage, capped at 2 pages; the rest are unconstrained +// Storage is capped at 2 pages; the other buckets retain harness defaults local twoPages(calldata) = { "vars": {}, "code": null, "message": msg, "calldata": calldata, - "bucket_totals": [2, 1000000, 1000000, 1000000], + "bucket_totals": {execution_data_gas: 2}, "gas_data": storageOnlyGasData, }; diff --git a/tests/integration/exploit/storage_page_limit/storage_page_limit.jsonnet b/tests/integration/exploit/storage_page_limit/storage_page_limit.jsonnet index bf9ac5cf..ad96ae2a 100644 --- a/tests/integration/exploit/storage_page_limit/storage_page_limit.jsonnet +++ b/tests/integration/exploit/storage_page_limit/storage_page_limit.jsonnet @@ -1,9 +1,8 @@ local msg = import 'templates/message.json'; local util = import 'templates/util.jsonnet'; -// storage now shares bucket 0 with the receipt buckets (message_receipt, -// nondet_output, event). To keep this test about storage alone, zero out the -// receipt-related prices so the only consumer of bucket 0 is storage. +// Storage shares execution_data_gas with the receipt buckets. Zero their prices +// so the only consumer of that bucket is storage local storageOnlyGasData = { storageUnitPrice: '1', receiptGasPerByte: '0', @@ -12,6 +11,7 @@ local storageOnlyGasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', }; @@ -30,8 +30,7 @@ local storageOnlyGasData = { "calldata": ||| {"": "write_2_pages", "args": []} |||, - // bucket 0 = storage (limited to 2 pages); bucket 1 (message_fee) unconstrained - "bucket_totals": [2, 1000000, 1000000, 1000000], + "bucket_totals": {execution_data_gas: 2}, "gas_data": storageOnlyGasData, }, { @@ -41,8 +40,7 @@ local storageOnlyGasData = { "calldata": ||| {"": "write_3_pages", "args": []} |||, - // bucket 0 = storage (limited to 2 pages); bucket 1 (message_fee) unconstrained - "bucket_totals": [2, 1000000, 1000000, 1000000], + "bucket_totals": {execution_data_gas: 2}, "gas_data": storageOnlyGasData, }, ], diff --git a/tests/integration/exploit/subtract_on_start_underflow/subtract_on_start_underflow.jsonnet b/tests/integration/exploit/subtract_on_start_underflow/subtract_on_start_underflow.jsonnet index bb0dd40b..e333d143 100644 --- a/tests/integration/exploit/subtract_on_start_underflow/subtract_on_start_underflow.jsonnet +++ b/tests/integration/exploit/subtract_on_start_underflow/subtract_on_start_underflow.jsonnet @@ -8,10 +8,10 @@ local util = import 'templates/util.jsonnet'; // (`oom().receipt().message().internal()`), which is delivered to the host as a // normal consume_result receipt instead of crashing during setup. // -// With default gas data, bucket 0 carries message_receipt (39) + -// nondet_output (32) = 71 of up-front cost. Funding it with less triggers the -// underflow. The expected stdout asserts the delivered -// `VMError("OOM receipt message internal")` receipt, guarding against a +// With default gas data, execution_data_gas carries message_receipt (7) + +// nondet_output (1088) = 1095 of up-front cost. Funding it below the first +// charge triggers the intended message-receipt underflow. The expected stdout +// asserts the delivered `VMError("out_of receipt message")`, guarding against a // regression back to the crash-during-setup behavior. {tags: util.features([['exploit'], ['fees']], 'stable') + ['python'], entry: util.addPaths([ @@ -21,6 +21,6 @@ local util = import 'templates/util.jsonnet'; "code": '${jsonnetDir}/${fileBaseName}.py', "message": msg + {"is_init": true}, "calldata": "{}", - "bucket_totals": [10, 1000000, 1000000, 1000000], + "bucket_totals": {execution_data_gas: 6}, }, ])} diff --git a/tests/integration/message/deploy/deploy.0.stdout b/tests/integration/message/deploy/deploy.0.stdout index da799ab2..d823fca2 100644 --- a/tests/integration/message/deploy/deploy.0.stdout +++ b/tests/integration/message/deploy/deploy.0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"calldata":{},"code":b#6e6f74207265616c6c79206120636f6e7472616374,"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":321,"salt_nonce":0,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalDeployMessage","use_balance":false,"value":0} +{"calldata":{},"code":b#6e6f74207265616c6c79206120636f6e7472616374,"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":34836,"on":"finalized","receipt_fee":2049,"salt_nonce":0,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalDeployMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/deploy_salt/deploy_salt.0.stdout b/tests/integration/message/deploy_salt/deploy_salt.0.stdout index fc9e2afe..86837ea8 100644 --- a/tests/integration/message/deploy_salt/deploy_salt.0.stdout +++ b/tests/integration/message/deploy_salt/deploy_salt.0.stdout @@ -1,3 +1,3 @@ 0xf539Cb83f077Cd01BDd1a4E002866dCC0D15D633 executed with `Return(null)` -{"calldata":{},"code":b#6e6f74207265616c6c79206120636f6e7472616374,"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":321,"salt_nonce":1,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalDeployMessage","use_balance":false,"value":0} +{"calldata":{},"code":b#6e6f74207265616c6c79206120636f6e7472616374,"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":34836,"on":"finalized","receipt_fee":2049,"salt_nonce":1,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalDeployMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.jsonnet b/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.jsonnet index 5d795c9a..8ca7a96a 100644 --- a/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.jsonnet +++ b/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.jsonnet @@ -1,7 +1,7 @@ local simple_deploy = import 'templates/simple_deploy.jsonnet'; local util = import 'templates/util.jsonnet'; -// Pin a per-phase timeunit floor above the emitted message's child timeunits. +// Pin each phase minimum above the emitted message's child timeunits. // gas_data replaces DEFAULT_GAS_DATA wholesale, so all required node fields are // restated here (kept minimal/deterministic, matching DEFAULT_GAS_DATA). local gasData = { @@ -12,9 +12,14 @@ local gasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + overlaySplitBps: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', // leader 5 / validator 10 (below) are rejected at emission. - minTimeUnitsPerPhase: '30', + minProposeTimeout: '30', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '30', + maxCommitTimeout: '340282366920938463463374607431768211455', }; // A single wildcard internal allocation that matches the emitted message, funded diff --git a/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.py b/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.py index b79360bf..8c6e892c 100644 --- a/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.py +++ b/tests/integration/message/internal_below_min_timeunits/internal_below_min_timeunits.py @@ -5,7 +5,6 @@ class Contract(gl.contract.Contract): def __init__(self): # Emits a single internal message. Its matched allocation funds child - # timeunits (leader 5, validator 10) below the node's minTimeUnitsPerPhase - # floor (30), so emission is rejected with `fee below_minimum` and the - # message is never issued. + # timeunits (leader 5, validator 10) below their phase minima (30), so + # emission is rejected and the message is never issued. gl.contract.get_at(gl.Address(b'\x30' * 20)).emit().foo(1, 2) diff --git a/tests/integration/message/message_count_cap/message_count_cap.0.stdout b/tests/integration/message/message_count_cap/message_count_cap.0.stdout new file mode 100644 index 00000000..612704ac --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.0.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/message_count_cap/message_count_cap.1.stdout b/tests/integration/message/message_count_cap/message_count_cap.1.stdout new file mode 100644 index 00000000..b7399080 --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.1.stdout @@ -0,0 +1 @@ +executed with `VMError("out_of message_fee total # internal")` diff --git a/tests/integration/message/message_count_cap/message_count_cap.2.stdout b/tests/integration/message/message_count_cap/message_count_cap.2.stdout new file mode 100644 index 00000000..612704ac --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.2.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/message_count_cap/message_count_cap.3.stdout b/tests/integration/message/message_count_cap/message_count_cap.3.stdout new file mode 100644 index 00000000..b7399080 --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.3.stdout @@ -0,0 +1 @@ +executed with `VMError("out_of message_fee total # internal")` diff --git a/tests/integration/message/message_count_cap/message_count_cap.4.stdout b/tests/integration/message/message_count_cap/message_count_cap.4.stdout new file mode 100644 index 00000000..612704ac --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.4.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/message_count_cap/message_count_cap.5.stdout b/tests/integration/message/message_count_cap/message_count_cap.5.stdout new file mode 100644 index 00000000..b7399080 --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.5.stdout @@ -0,0 +1 @@ +executed with `VMError("out_of message_fee total # internal")` diff --git a/tests/integration/message/message_count_cap/message_count_cap.jsonnet b/tests/integration/message/message_count_cap/message_count_cap.jsonnet new file mode 100644 index 00000000..926d7bb9 --- /dev/null +++ b/tests/integration/message/message_count_cap/message_count_cap.jsonnet @@ -0,0 +1,16 @@ +local simpleDeploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; + +local base = simpleDeploy.run('${jsonnetDir}/../send_message/send_message.py'); +{tags: util.features([['message', 'send'], ['fees']], 'stable') + ['python'], + entry: util.addPaths([ + base {bucket_totals: {submitted_messages_count: 1}}, + base {bucket_totals: {submitted_messages_count: 0}}, + // 64-byte array frame + one 1888-byte conservatively encoded message + base {bucket_totals: {submitted_messages: 1952}}, + base {bucket_totals: {submitted_messages: 1951}}, + // 1095 startup + 12 storage + 1953 message receipt gas + base {bucket_totals: {execution_data_gas: 3060}}, + base {bucket_totals: {execution_data_gas: 3059}}, + ]), +} diff --git a/tests/integration/message/nested_allocation_budget/nested_allocation_budget.0.stdout b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.0.stdout new file mode 100644 index 00000000..9aa08417 --- /dev/null +++ b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.0.stdout @@ -0,0 +1,2 @@ +executed with `Return(null)` +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo"},"fee_params":{"execution_budget_per_round":1,"leader_timeunits_allocation":1,"max_price_gen_per_time_unit":2,"receipt_fee_max_gas_price":1,"rotations":[0],"storage_fee_max_gas_price":1,"validator_timeunits_allocation":1},"message_fee":75,"on":"finalized","receipt_fee":3617,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000054000000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000000000000000000000000000000000000000000000000000000d00000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/nested_allocation_budget/nested_allocation_budget.jsonnet b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.jsonnet new file mode 100644 index 00000000..378d97d2 --- /dev/null +++ b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.jsonnet @@ -0,0 +1,58 @@ +local simple_deploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; + +// Use the deployed 15% split to prove the emitted budget covers both the +// consensus primary reserve and the carried direct-child budgets +local gasData = { + storageUnitPrice: '1', + receiptGasPerByte: '1', + gasPerChangedSlot: '1', + intrinsicGas: '0', + bootloaderOverhead: '0', + fixedProposeReceiptGas: '0', + fixedMessageRevealGas: '0', + lockedReceiptGasPrice: '1', + overlaySplitBps: '1500', + receiptWrapperBytes: '1024', + genPerTimeUnit: '0', + minProposeTimeout: '1', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '1', + maxCommitTimeout: '340282366920938463463374607431768211455', + messageBudgetFloor: '0', +}; + +local params = { + execution_budget_per_round: 1, + rotations: [0], + leader_timeunits_allocation: 1, + validator_timeunits_allocation: 1, + max_price_gen_per_time_unit: 2, + storage_fee_max_gas_price: 1, + receipt_fee_max_gas_price: 1, +}; + +local child(budget, children=[], recipient=null) = { + budget: budget, + recipient: recipient, + call_key: null, + on: 'finalized', + fee_params: {Internal: params}, + children: children, +}; + +local alloc = child(100, [ + child(30, [child(13)]), + child(30, [], 'AwAAAAAAAAAAAAAAAAAAAAAAAAA='), +]); + +{ + tags: util.features([['message', 'send'], ['fees']], 'stable') + ['python'], + entry: util.addPaths([ + simple_deploy.run('${jsonnetDir}/${fileBaseName}.py') { + bucket_totals: {message_fee: 100}, + gas_data: gasData, + message_fee_allocation: [alloc], + }, + ]), +} diff --git a/tests/integration/message/nested_allocation_budget/nested_allocation_budget.py b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.py new file mode 100644 index 00000000..b43bbbe9 --- /dev/null +++ b/tests/integration/message/nested_allocation_budget/nested_allocation_budget.py @@ -0,0 +1,7 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl + + +class Contract(gl.contract.Contract): + def __init__(self): + gl.contract.get_at(gl.Address(b'\x30' * 20)).emit().foo() diff --git a/tests/integration/message/send_message/send_message.0.stdout b/tests/integration/message/send_message/send_message.0.stdout index 0bf3957b..612704ac 100644 --- a/tests/integration/message/send_message/send_message.0.stdout +++ b/tests/integration/message/send_message/send_message.0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":34836,"on":"finalized","receipt_fee":225,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"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":34836,"on":"finalized","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/send_message_eth/send_message_eth.0.stdout b/tests/integration/message/send_message_eth/send_message_eth.0.stdout index f3af998f..346a1645 100644 --- a/tests/integration/message/send_message_eth/send_message_eth.0.stdout +++ b/tests/integration/message/send_message_eth/send_message_eth.0.stdout @@ -1,3 +1,3 @@ 100 executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"calldata":b#29e99f07000000000000000000000000000000000000000000000000000000000000000a,"fee_params":{"gas_limit":1606938044258990275541962092341162602522202993782792835301376,"max_gas_price":0},"message_fee":0,"receipt_fee":193,"type":"ExternalMessage","value":30} +{"address":addr#3030303030303030303030303030303030303030,"calldata":b#29e99f07000000000000000000000000000000000000000000000000000000000000000a,"fee_params":{"gas_limit":1606938044258990275541962092341162602522202993782792835301376,"max_gas_price":0},"message_fee":0,"receipt_fee":737,"type":"ExternalMessage","value":30} diff --git a/tests/integration/message/send_message_on/send_message_on.0_0.stdout b/tests/integration/message/send_message_on/send_message_on.0_0.stdout index 2768d949..9950a323 100644 --- a/tests/integration/message/send_message_on/send_message_on.0_0.stdout +++ b/tests/integration/message/send_message_on/send_message_on.0_0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":1606938044258990275541962092341162602522202993782792835301376,"receipt_fee_max_gas_price":1606938044258990275541962092341162602522202993782792835301376,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":1606938044258990275541962092341162602522202993782792835301376,"validator_timeunits_allocation":5},"message_fee":174180,"on":"decided","receipt_fee":225,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000,"type":"InternalMessage","use_balance":false,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"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":34836,"on":"decided","receipt_fee":1953,"subtree":b#00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000100000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004,"type":"InternalMessage","use_balance":false,"value":0} diff --git a/tests/integration/message/use_balance_below_min/use_balance_below_min.jsonnet b/tests/integration/message/use_balance_below_min/use_balance_below_min.jsonnet index 83cb8c55..39bb7bfd 100644 --- a/tests/integration/message/use_balance_below_min/use_balance_below_min.jsonnet +++ b/tests/integration/message/use_balance_below_min/use_balance_below_min.jsonnet @@ -1,7 +1,7 @@ local deploy_then = import 'templates/simple_deploy_then_write.jsonnet'; local util = import 'templates/util.jsonnet'; -// Pin a per-phase timeunit floor (30) above the emitted message's timeunits (5). +// Pin both phase minima (30) above the emitted message's timeunits (5). // gas_data replaces DEFAULT_GAS_DATA wholesale, so all required node fields are // restated here (kept minimal/deterministic, matching DEFAULT_GAS_DATA). local gasData = { @@ -12,11 +12,16 @@ local gasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + overlaySplitBps: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', - minTimeUnitsPerPhase: '30', + minProposeTimeout: '30', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '30', + maxCommitTimeout: '340282366920938463463374607431768211455', }; -// Ample balance so the rejection isolates the timeunit floor, not the balance. +// Ample balance so the rejection isolates the phase bounds, not the balance. local extra = { 'balances': { 'AQAAAAAAAAAAAAAAAAAAAAAAAAA=': 1000000, diff --git a/tests/integration/message/use_balance_below_min/use_balance_below_min.py b/tests/integration/message/use_balance_below_min/use_balance_below_min.py index f6ee0cac..8d25d5a4 100644 --- a/tests/integration/message/use_balance_below_min/use_balance_below_min.py +++ b/tests/integration/message/use_balance_below_min/use_balance_below_min.py @@ -2,8 +2,8 @@ import genlayer as gl from genlayer.vm.public_abi import Permissions -# leader/validator timeunits (5) are below the node's minTimeUnitsPerPhase floor -# (30, set in the jsonnet), so metering rejects the emission. +# Leader/validator timeunits (5) are below their phase minima (30, set in the +# jsonnet), so metering rejects the emission. _PARAMS = gl.chain.InternalMessageParams( leader_time_units_allocation=5, validator_time_units_allocation=5, @@ -23,8 +23,7 @@ def __init__(self): @gl.public.write def do_emit(self): - # The min-timeunits floor is enforced on the balance-funded path too, so - # emission aborts with the `fee below_minimum` VMError. + # Phase bounds are enforced on the balance-funded path too. gl.contract.get_at(gl.Address(b'\x30' * 20)).emit( use_balance=True, fee_params=_PARAMS ).foo(1, 2) diff --git a/tests/integration/message/use_balance_budget_too_low/use_balance_budget_too_low.jsonnet b/tests/integration/message/use_balance_budget_too_low/use_balance_budget_too_low.jsonnet index dd6d8979..aa59c1b4 100644 --- a/tests/integration/message/use_balance_budget_too_low/use_balance_budget_too_low.jsonnet +++ b/tests/integration/message/use_balance_budget_too_low/use_balance_budget_too_low.jsonnet @@ -12,8 +12,13 @@ local gasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + overlaySplitBps: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', - minTimeUnitsPerPhase: '0', + minProposeTimeout: '1', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '1', + maxCommitTimeout: '340282366920938463463374607431768211455', messageBudgetFloor: '2000', }; diff --git a/tests/integration/message/use_balance_no_alloc/use_balance_no_alloc.0_0_0.stdout b/tests/integration/message/use_balance_no_alloc/use_balance_no_alloc.0_0_0.stdout index 3bdc724a..d527a2c5 100644 --- a/tests/integration/message/use_balance_no_alloc/use_balance_no_alloc.0_0_0.stdout +++ b/tests/integration/message/use_balance_no_alloc/use_balance_no_alloc.0_0_0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":161,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_ok/use_balance_ok.0_0.stdout b/tests/integration/message/use_balance_ok/use_balance_ok.0_0.stdout index 3bdc724a..d527a2c5 100644 --- a/tests/integration/message/use_balance_ok/use_balance_ok.0_0.stdout +++ b/tests/integration/message/use_balance_ok/use_balance_ok.0_0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":161,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_sandbox/use_balance_sandbox.0_0.stdout b/tests/integration/message/use_balance_sandbox/use_balance_sandbox.0_0.stdout index cd1659a7..033ffe17 100644 --- a/tests/integration/message/use_balance_sandbox/use_balance_sandbox.0_0.stdout +++ b/tests/integration/message/use_balance_sandbox/use_balance_sandbox.0_0.stdout @@ -1,3 +1,3 @@ sandbox: emitted executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":161,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,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":39976,"on":"finalized","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_scaled/use_balance_scaled.0_0.stdout b/tests/integration/message/use_balance_scaled/use_balance_scaled.0_0.stdout index 531c8520..d8cd776d 100644 --- a/tests/integration/message/use_balance_scaled/use_balance_scaled.0_0.stdout +++ b/tests/integration/message/use_balance_scaled/use_balance_scaled.0_0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":3,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":45116,"on":"finalized","receipt_fee":161,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":1024,"leader_timeunits_allocation":5,"max_price_gen_per_time_unit":3,"receipt_fee_max_gas_price":20,"rotations":[4,4,4,4,4],"storage_fee_max_gas_price":20,"validator_timeunits_allocation":5},"message_fee":47837,"on":"decided","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_scaled/use_balance_scaled.jsonnet b/tests/integration/message/use_balance_scaled/use_balance_scaled.jsonnet index 72e0c285..372bd568 100644 --- a/tests/integration/message/use_balance_scaled/use_balance_scaled.jsonnet +++ b/tests/integration/message/use_balance_scaled/use_balance_scaled.jsonnet @@ -12,8 +12,13 @@ local gasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + overlaySplitBps: '1500', + receiptWrapperBytes: '1024', genPerTimeUnit: '7', - minTimeUnitsPerPhase: '0', + minProposeTimeout: '1', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '1', + maxCommitTimeout: '340282366920938463463374607431768211455', messageBudgetFloor: '0', }; diff --git a/tests/integration/message/use_balance_scaled/use_balance_scaled.py b/tests/integration/message/use_balance_scaled/use_balance_scaled.py index c71cab93..7e904574 100644 --- a/tests/integration/message/use_balance_scaled/use_balance_scaled.py +++ b/tests/integration/message/use_balance_scaled/use_balance_scaled.py @@ -2,12 +2,13 @@ import genlayer as gl from genlayer.vm.public_abi import Permissions -# Proves the balance-funded floor scales with the GUEST cap, not the node's live -# genPerTimeUnit. With consensusTerm=5140 and executionTerm=1024*29=29696: -# fee = max_price_gen_per_time_unit * consensusTerm + executionTerm -# = 3 * 5140 + 29696 = 45116 +# Proves the balance-funded floor scales with the GUEST cap and grosses up only +# the time-unit pool. With timeUnitPool=3*5140, overlay=floor(15420*1500/8500), +# and executionTerm=1024*29: +# primary = 15420 + 2721 + 29696 = 47837 +# The per-message fee is 47837 for both decided and finalized emissions # The jsonnet sets node.genPerTimeUnit=7; had the balance path used it the fee -# would be 7*5140 + 29696 = 65676. The golden's 45116 confirms the cap is used. +# would be 7*5140 + floor(35980*1500/8500) + 29696 = 72025 _PARAMS = gl.chain.InternalMessageParams( leader_time_units_allocation=5, validator_time_units_allocation=5, @@ -28,5 +29,5 @@ def __init__(self): @gl.public.write def do_emit(self): gl.contract.get_at(gl.Address(b'\x30' * 20)).emit( - use_balance=True, fee_params=_PARAMS + on='decided', use_balance=True, fee_params=_PARAMS ).foo(1, 2) diff --git a/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.0_0.stdout b/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.0_0.stdout index 7c3b7a84..13a164ff 100644 --- a/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.0_0.stdout +++ b/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.0_0.stdout @@ -1,2 +1,2 @@ executed with `Return(null)` -{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":0,"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":10280,"on":"finalized","receipt_fee":161,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} +{"address":addr#3030303030303030303030303030303030303030,"call_key":b#666f6f0000000000000000000000000000000000000000000000000000000000,"calldata":{"":"foo","args":[1,2]},"fee_params":{"execution_budget_per_round":0,"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":10280,"on":"finalized","receipt_fee":1121,"subtree":b#,"type":"InternalMessage","use_balance":true,"value":0} diff --git a/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.jsonnet b/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.jsonnet index e4f4da29..fd45c6a5 100644 --- a/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.jsonnet +++ b/tests/integration/message/use_balance_zero_budget/use_balance_zero_budget.jsonnet @@ -13,8 +13,13 @@ local gasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + overlaySplitBps: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', - minTimeUnitsPerPhase: '0', + minProposeTimeout: '1', + maxProposeTimeout: '340282366920938463463374607431768211455', + minCommitTimeout: '1', + maxCommitTimeout: '340282366920938463463374607431768211455', messageBudgetFloor: '2000', }; diff --git a/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0.stdout b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0.stdout new file mode 100644 index 00000000..7d73027c --- /dev/null +++ b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0.stdout @@ -0,0 +1 @@ +executed with `Return(null)` diff --git a/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0_0.stdout b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0_0.stdout new file mode 100644 index 00000000..709efa66 --- /dev/null +++ b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.0_0.stdout @@ -0,0 +1 @@ +executed with `VMError("leader_fault nondet_output malformed")` diff --git a/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.jsonnet b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.jsonnet new file mode 100644 index 00000000..7045bcef --- /dev/null +++ b/tests/integration/nondet-consensus/leader_errors/malformed_leader_public_data.jsonnet @@ -0,0 +1,11 @@ +local simple = import 'templates/simple_deploy_then_write.jsonnet'; +local util = import 'templates/util.jsonnet'; +// The envelope itself does not decode, so the fault is raised before the VM +// starts. It is a leader fault like any other, hence fatal +{tags: util.features([['nondet', 'consensus', 'leader', 'error']], 'stable') + ['python'], + entry: util.addPaths([simple.run('${jsonnetDir}/simple.py', 'bar') { + next: [super.next[0] { + modes: 'vs', + leader_public_data_raw: [255], + }], +}])} diff --git a/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_1.stdout b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_1.stdout new file mode 100644 index 00000000..6420cc04 --- /dev/null +++ b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_1.stdout @@ -0,0 +1 @@ +executed with `VMError("out_of receipt nondet_output")` diff --git a/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_2.stdout b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_2.stdout new file mode 100644 index 00000000..6420cc04 --- /dev/null +++ b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_2.stdout @@ -0,0 +1 @@ +executed with `VMError("out_of receipt nondet_output")` diff --git a/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_3.stdout b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_3.stdout new file mode 100644 index 00000000..17c96437 --- /dev/null +++ b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.0_3.stdout @@ -0,0 +1,2 @@ +executed with `VMError("leader_fault nondet_output malformed")` +nondet disagreement: 0 diff --git a/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.jsonnet b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.jsonnet index 1a2b86a7..7f493b68 100644 --- a/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.jsonnet +++ b/tests/integration/nondet-consensus/output_fee_cap/output_fee_cap.jsonnet @@ -2,16 +2,31 @@ local simple = import 'templates/simple_deploy_then_write.jsonnet'; local util = import 'templates/util.jsonnet'; { - tags: util.features([['nondet', 'consensus', 'leader'], ['fees']], 'stable') + ['python'], + tags: util.features([['nondet', 'consensus', 'leader', 'malicious'], ['fees']], 'stable') + ['python'], entry: util.addPaths([ simple.run('${jsonnetDir}/${fileBaseName}.py', 'main') { - next: [ - super.next[0] { + next: + local exact = super.next[0] { modes: 'lvs', - // VMError byte + "out_of receipt nondet_output" - bucket_totals: [1000000000, 1000000000, 29, 1000000000], - }, - ], + // 64-byte frame + one 34-byte compact VMError output + bucket_totals: { + nondet_outputs: 98, + // 71 message startup gas + 1024 wrapper + 34 output bytes + execution_data_gas: 1129, + }, + }; + [ + exact, + exact {bucket_totals+: {nondet_outputs: 97}}, + exact {bucket_totals+: {execution_data_gas: 1128}}, + // A leader publishing an output its own bucket could not have + // paid for: the substitution no longer matches the proposal, so + // validator and sync must both fault + exact { + modes: 'vs', + leader_nondet: [{kind: 'return', value: std.repeat('x', 200)}], + }, + ], }, ]), } diff --git a/tests/integration/storage/sandbox_fold_limit/sandbox_fold_limit.jsonnet b/tests/integration/storage/sandbox_fold_limit/sandbox_fold_limit.jsonnet index 733a3285..92145869 100644 --- a/tests/integration/storage/sandbox_fold_limit/sandbox_fold_limit.jsonnet +++ b/tests/integration/storage/sandbox_fold_limit/sandbox_fold_limit.jsonnet @@ -9,6 +9,7 @@ local storageOnlyGasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', }; @@ -17,6 +18,5 @@ local base = simple.run('${jsonnetDir}/${fileBaseName}.py'); entry: util.addPaths([base + { // Validator and sync modes would make their multi-GiB allocations concurrently. modes: 'l', - bucket_totals: [1000000000, 1000000000, 1000000000, 1000000000], gas_data: storageOnlyGasData, }])} diff --git a/tests/integration/storage/zero_fee_ram_bound/zero_fee_ram_bound.jsonnet b/tests/integration/storage/zero_fee_ram_bound/zero_fee_ram_bound.jsonnet index dd8122e6..91ff0270 100644 --- a/tests/integration/storage/zero_fee_ram_bound/zero_fee_ram_bound.jsonnet +++ b/tests/integration/storage/zero_fee_ram_bound/zero_fee_ram_bound.jsonnet @@ -11,6 +11,7 @@ local freeStorageGasData = { bootloaderOverhead: '0', fixedProposeReceiptGas: '0', fixedMessageRevealGas: '0', + receiptWrapperBytes: '1024', genPerTimeUnit: '0', }; @@ -19,6 +20,5 @@ local base = simple.run('${jsonnetDir}/${fileBaseName}.py'); entry: util.addPaths([base + { // Validator and sync modes would make their multi-GiB allocations concurrently. modes: 'l', - bucket_totals: [1000000000, 1000000000, 1000000000, 1000000000], gas_data: freeStorageGasData, }])}