Repository navigation
Conversation
When a system contract (privileged, sysio or sysio.*) notifies or inline-calls a contract outside the system accounts, that contract pays the CPU of its own code. Frames below it bill the same contract, so a contract it forwards to or calls is billed to it. An explicit payer on the input action still pays for everything below it. The receipt's cpu_usage_us holds one entry per input action and then one per frame where billing switched, in execution order, and the transaction's CPU is their sum. A validator derives each entry's account by running the transaction: an entry the receipt leaves out bills nothing, so the receipt it builds differs from the block's and the block is rejected, and an extra entry fails the transaction. A contract billed this way pays no NET. Change-Id: I076e6c34f81034fe4a456c99c6795a46c1cfd478
The test that leaves a receiver entry out of a receipt now moves that entry's CPU onto the input entry. The input entry alone can be under the CPU minimum, and on fast builds the minimum check rejected the block before the receipt comparison ran. Change-Id: I79ec207a7bdb550e657ee986cef6535fc97d3989
| if (bill_objectively) { | ||
| bill_receiver(e, cpu); |
There was a problem hiding this comment.
[P1] Remove receiver subjective charges when finalization rejects the transaction
update_billed_cpu_time() runs before the final CPU checks, so trace->except is still empty here when execution succeeds but finalization will fail. Receiver entries are added to accounts_billing and committed to the subjective cache; is_cpu_updated then prevents the controller's exception handler from rebuilding failure billing.
I reproduced this with payer billing enabled on a speculative node: a transfer to game, followed by a cheap action on a zero-CPU contract, fails finalization yet leaves game with pending subjective CPU. These rejected transactions cannot be included in a block to clear their charges, so repeated submissions can throttle an unrelated receiving contract. Please defer publishing receiver subjective charges until the final checks succeed, or roll them back and rebuild input-only failure billing.
There was a problem hiding this comment.
Fixed in bbabd2c. finalize now bills the CPU, runs the final checks and records the usage before it bills anything subjectively. A failed check undoes the billing (unbill_cpu), so the controller bills the transaction as a failed one: no receiver entries, CPU on the input entries, and a failure's subjective bill for the payers and the first authorizer. Before, such a transaction also kept its payers' pending bill on a speculative node and billed nothing on a producing one. Test: transaction_failing_its_final_checks_bills_no_receiver, on speculative and producing blocks, with and without payer billing.
| accounts_billing.try_emplace(e.account, account_billing{ .cpu_limit_us = e.cpu_limit_us, | ||
| .cpu_greylisted = e.cpu_greylisted, | ||
| .pays_net = false }) |
There was a problem hiding this comment.
[P2] Refresh receiver CPU limits before final validation
e.cpu_limit_us was captured at frame entry. A measured receiver is inserted into accounts_billing only during update_billed_cpu_time(), after the finalize() loop that refreshes input payers, so this cached limit is used even if later actions granted CPU.
I reproduced this with a short transfer handler on idle with zero initial CPU, followed by a CPU grant in the same transaction: finalization rejects the receiver against its old zero limit. Reversing the action order succeeds. This also prevents a receiver from using the execution leeway to obtain resources during its execution. Please refresh receiver CPU limits from the final transaction state, including pending-block usage decay, before validate_available_account_cpu().
There was a problem hiding this comment.
Fixed in bbabd2c. A receiver entry no longer carries its entry-time limit into accounts_billing. finalize reads each receiver's limit from the final state, decayed to the pending block, before validate_available_account_cpu. The limit read as the receiver's first frame starts still sets its frames' deadline, with the leeway. Test: receiver_limit_includes_cpu_granted_in_the_transaction, with the grant after the handler and before it.
finalize billed a successful transaction subjectively before its final CPU checks ran. When a check then failed, the controller could not rebill the transaction, so its pending subjective bill stayed in place, receivers included, although the transaction never reaches a block. finalize now bills the CPU, runs the checks and records the usage, and bills subjectively only after all of them pass. A failed check undoes the billing, and the controller bills the transaction as a failed one: its CPU stays on the input entries and no receiver is billed. Payers of such a transaction now get a failure's subjective bill too, on producing nodes as well. A contract billed for its own code is checked against its CPU limit as the transaction leaves it, read again from the final state with its usage decayed to the pending block, so CPU granted to it later in the transaction counts. The limit read as its first frame starts still sets the frames' deadline. New tests cover both, and receiver billing for a privileged versus an unprivileged sysio.* receiver, two inputs with different authorizers, a contract reached twice, a receiver that also pays for another input, an explicit payer on one of two inputs, a receiver frame stopped by max_cpu_usage_ms, a greylisted receiver, and code set or cleared earlier in the same transaction. Change-Id: Ib8d80437784b4d42228a3dd0feb815bd268fc804
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed bbabd2c. Both previous findings are fixed; no new actionable findings. The isolated review build passed 30 focused tests and 495 assertions on each of sys-vm and sys-vm-jit, including both original reproductions.
The entry that pays for a frame now also pays for the first `notify_cpu_stipend` microseconds of a contract the frame notifies. The contract's entry holds only its CPU past that. `notify_cpu_stipend` is a new `chain_config` parameter: - genesis default 100 µs; - changed through `setparams`; - 0 means no stipend; - at most `max_transaction_cpu_usage`; - appended as id 17, so no existing id moves. A switch gets one stipend, shared by every frame billed to the contract's entry. An action sent to a contract, such as through `sysio.msig::exec`, gets none, just as calling the contract directly gets none. The receipt format is unchanged. A contract whose code stays within the stipend has an entry of 0, and an entry of 0 bills nothing. Only the producer applies the stipend: validators bill the receipt's entries as given. A notified frame runs for what is left of the stipend, then for the contract's remaining CPU. On sys-vm-oc, a compile on a code-cache miss now pauses billing, as module instantiation already does on the other runtimes, so a contract's first run is not billed for loading its module. `sysio.system` packs the new parameter. `sysio.bios`, `sysio.system`, `params_test` and `cpu_receiver_test` are rebuilt with the matching wire-cdt, and SHiP's `chain_config` gains the field. `cpu_receiver_test` drops its context-free inline, which master removed, and gains `spinsend:<n>`. The msig test proposes with `sysio.payer`, as master now requires. The trace_api docs say that `cpu_usage_us` also appears on receiver frames. The deep-mind log, the snapshot compatibility files, the consensus blockchain data and the sys-util snapshot info expectations are regenerated. Change-Id: Iba358c8dec580c897389828eeff54d66f70761d7
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed e2814b1, including the merge with master, notification CPU stipend, configuration serialization, and optimized-runtime compilation billing. Both earlier findings remain fixed; no new actionable findings. An isolated rebuild of 99 translation units passed all 47 focused receiver CPU billing, parameter, and system-host tests (894 assertions) on each of sys-vm, sys-vm-jit, and sys-vm-oc.
…ys-cpu Brings in #758, #759, #760 and #761. `sysio.system` is rebuilt with the wire-cdt of #132, so `sysio.system.wasm` carries #761's changes and the new parameter. Every other contract and ABI built with that wire-cdt matches master's byte for byte. `get_cached_descriptor`, which this branch adds to the sys-vm-oc code cache, drops the top-level `const` on its return type, as #758 does for the functions beside it. The deep-mind log, the snapshot compatibility files, the consensus blockchain data and the sys-util snapshot info expectations still pass unchanged.
CI run 42879946461 on e2814b1 failed `checktime_speculative_max_trx_test` on sys-vm-oc in every Linux job: its first push threw `deadline_exception` instead of `tx_cpu_usage_exceeded` from a 25 ms max-transaction-time. That push is the pause contract's first run, which in sys-vm-oc-forced mode compiles the contract, about 0.9 s here. Master bills the compile to the transaction, which then fails on its 25 ms limit. This branch pauses billing for the compile, as instantiation already is, so that loading or compiling a module is never billed on any runtime: the transaction's deadlines move past the pause, but the push's 500 ms block deadline does not, so the push stops there. With only that pause reverted, the push fails on its 25 ms limit as on master, 5 of 5 runs each way. The test now runs the contract once before its timings, through a `warm_pause_contract` helper its interrupt half also uses. Two tests pause a frame through `substitute_apply`, the same on every runtime, first the input action's frame and then a notified contract's. One checks that the frame's CPU and deadline leave the pause out. The other checks that a pause past the block deadline stops the transaction with `deadline_exception`, which a producer retries. Change-Id: Id65e27318f3da9204a2d77d36271479f08af525f
CI run 42941122275 failed `stipend_is_shared_by_the_frames_billed_to_the_contract` on asan and ubsan. Its last check compared entries from separate one-frame and two-frame transactions, so a host that slowed one batch could fail it. The test now checks, within each transaction, that the entry leaves out of its two frames' elapsed time less than 1.5 stipends: one stipend plus the time outside the frames, where a stipend per frame would leave out two. Change-Id: Iecf2452a0cb08c78eace93a9a3b7cd63cbe425bd
Rule
Each executed frame (one receiver's run of one action) is billed to one receipt entry.
The RAM rule uses the same system test: a privileged sysio or sysio.* contract may bill RAM to other accounts.
An explicit payer pays for every frame below its action, whoever it is. With
sysioas the explicit payer, sysio pays for all of them. Implicit transactions (onblock) follow the explicit-payer rule: every frame bills the input payer, sysio.Examples:
sysio.msig::execinto a contract bills that contract from the start.max_cpu_usage_ms.Stipend
notify_cpu_stipendis a newchain_configparameter in microseconds, 100 by default at genesis. Governance changes it throughsetparams, and 0 turns the stipend off. Validation requiresnotify_cpu_stipend <= max_transaction_cpu_usage; there is no other bound. It is appended as id 17, aftermax_kv_secondary_key_size, inchain_config, its packing, SHiP and CDT'sblockchain_parameters, so no existing id moves.notify_cpu_stipend, and the contract pays for the rest. Nothing aborts at the stipend, and no contract needs a handler.sysio.msig::exec, or an inline action from a system contract) bills the contract from the start, as calling it directly does.deadline_exception, which a producer retries in a later block.Receipt
cpu_usage_uskeeps its type. After the input entries, it gets one entry per frame where billing switched, in the order those frames started. An entry covers its frame and the frames below it billed to the same contract, past the stipend, plus the stipends that contract pays for. The transaction's CPU is the sum of all entries. Transactions without a switch add no bytes, and no account names are stored.A validator requires at least one entry per input action before execution. After execution, it bills an entry the receipt leaves out as 0. The receipt it builds then differs from the block's, and block validation rejects the block, in every validation mode. A receipt with more entries than switched frames fails the transaction.
Billing
tx_cpu_usage_exceededon the account CPU limit. Read-only transactions skip these limits.disable-subjective-payer-billing=false), the receiver is also billed like any payer, but only once if it is that authorizer. A receiver's subjective bill counts against its frames as a payer's counts against its action.finalize's CPU checks.finalizebills subjectively only after those checks and the usage record pass, and a failed check undoes the billing, so the controller bills the transaction as a failed one. Its payers are billed the same way; before, such a transaction kept a pending bill on a speculative node and got no subjective bill on a producing one.init(). They are read when a receiver's first frame starts, for its deadline, and again from the transaction's final state for the final checks, so CPU granted to the receiver later in the transaction counts.action_trace::cpu_usage_usis filled for each frame where billing switched.Consensus
A hard change with no protocol feature: the chain has not launched. The new
chain_configfield changes the genesis and so the chain id, SHiP'schain_config, andsetparamsinsysio.biosandsysio.system. It also changes the layout ofsysio.system'sglobalrow, which derives fromblockchain_parameters.Tests
receiver_cpu_billing_testsuses a newcpu_receiver_testcontract withsysio.tokenandsysio.msig. The fixture setsnotify_cpu_stipendto 1 ms throughsetparams, so that a frame inside or past it does not depend on the host's speed. A test that needs a handler past the stipend first measures a loop count that keeps it there, since runtimes differ by orders of magnitude. The suite passes on sys-vm, sys-vm-jit and sys-vm-oc.checktime_speculative_max_trx_testnow runs its contract once before its timings, as its interrupt half already did. This fixes CI run 42879946461, where the test failed on sys-vm-oc in all five Linux jobs. Its first push compiles the contract in sys-vm-oc-forced mode, the only mode that compiles inside a transaction. Master bills that compile against the push's 25 ms limit. This change keeps it unbilled, so that loading or compiling a module is never billed on any runtime, and the push stops at its 500 ms block deadline instead. Five runs each on sys-vm-oc without the warm-up:deadline_exceptionat the 500 ms block deadline after 0.79 to 0.90 stx_cpu_usage_exceededafter 0.89 to 0.90 sThe stipend's behaviour:
many_notifications_in_one_block_cost_the_contract_nothingnotified_contract_pays_only_past_the_stipend,stipend_follows_setparamscontract_without_cpu_receives_within_the_stipend(balances unchanged),receiver_over_its_limit_fails_on_account_cpu_limitcold_module_load_takes_none_of_the_stipenddeadline_exceptionpause_in_a_frame_is_left_out_of_its_cpu_and_deadline,pause_past_the_block_deadline_stops_at_the_block_deadlinezero_entry_leaves_the_contracts_cpu_window_alonestipend_is_shared_by_the_frames_billed_to_the_contractgame_pays_the_winners_stipend_and_the_winner_the_restmsigexec gets no stipendmsig_exec_bills_the_called_contract_without_a_stipendsetparams; 0 turns it off; abovemax_transaction_cpu_usageis refusednotify_cpu_stipend_starts_at_100_us,stipend_follows_setparams,stipend_above_the_transaction_limit_is_rejected,params_testsvalidator_bills_the_producers_splitThe suite also covers:
max_cpu_usage_ms;sysio.*receiver;max_cpu_usage_ms;Each billing test also checks that the validator's CPU and NET windows match the producer's.
The tester bills its explicit pushes one entry per input action, as before, so a contract billed for its own code is billed nothing there.
Paired changes
Land these together: wire-cdt first, then wire-sysio right after it, then wire-libraries-ts and wire-tools-ts.
blockchain_parametersgainsnotify_cpu_stipend. Add notify_cpu_stipend to Blockchain Parameters wire-cdt#132SysioContractTypes.tsregenerated. Regenerate Sysio Contract Types for the Notify CPU Stipend wire-libraries-ts#153notify_cpu_stipend. Render the Notify CPU Stipend in the Cluster Genesis wire-tools-ts#185Consumers of the receipt and per-action CPU
cpu_usage_uskeeps itsvaruint32[]type; action traces carry no CPU field.cpu_usage_usnow also appears on receiver frames.get_block_response_receipt.cpu_usage_usis already an array;p2p/Types.tsandchain/Transaction.tskeep a singleuint32.cpu_usage_usas a scalar, and the transaction page reads the first action's CPU or the receipt as a scalar.cpu_usage_usby action index must read only the firsttotal_actions()entries. Keep Subjective CPU Charged to Authorizers of sysio.*-Paid Actions #750'sbill_sysio_paid_authorizersrequires exactlytotal_actions()entries; whichever of the two lands second updates it.Follow-ups
sysio.-prefixed names in nativenewaccount, since the rule keys on the prefix.