Docs: ROA resource model overview - #583
Conversation
User-facing documentation for Resource Owner Allocation, covering the payer model, policies, and how CPU/NET/RAM are provisioned on Wire. The payer of an action is chosen explicitly: authorization[0] carrying the reserved sysio.payer permission names the payer, otherwise the payer is the contract being called. Ordinary transactions do not name one, so the contract pays and the signing account is never charged or limit-checked. An account can elect to pay for itself, and then needs its own allocation. Also documents node owner tiers and budgets, multi-issuer policy stacking, the proportional-share formula and where its denominator comes from, the two layers of spam control, and worked scenarios for users, contract deployment, trading, and onboarding. Includes a comparison to EOS staking, REX, and PowerUp for readers arriving from Antelope. Behavioural claims were verified against the implementation: contract-as-payer billing, the failure mode of an unprovisioned contract, RAM-free token transfers, and multi-issuer stacking.
Adds four subsections under Policies covering what a node owner actually receives and needs. What registration provisions: the 10% sysio RAM pool grant scales with tier and is never reclaimable; the personal RAM/NET/CPU weights are flat across tiers and land in a self-issued policy with time_block 1. Flat components against non-flat budgets cost a tier-3 owner 4.77% of its allocation versus 0.0036% for tier 1. What a node owner needs to operate: nothing. The four policy actions resolve payer() to sysio.roa, and every row the contract writes for registration and policy management is billed to sysio.roa, so an owner with zero CPU, zero NET and zero spare RAM can still issue. The exception is tier-1 newuser, whose sponsors and sponsorcount rows are the only writes charged to a node owner. What differs between tiers: policy mechanics are identical; newuser is the only tier-gated capability. Where network RAM comes from: sysio.roa holds a fixed ~157 MB for its own rows while sysio grows to ~7.98 GB as owners register, funding roughly 6.97 million account creations. That gap is why newuser bills sponsorship rows to the sponsoring owner rather than the contract.
|
|
||
| Resources reach contract accounts through **policies** issued by **node owners**. A node owner | ||
| holds a fixed share of the network's resource capacity, determined by their tier, and grants | ||
| slices of it to accounts via the `sysio.roa` contract. A contract with a policy works. A contract |
There was a problem hiding this comment.
The claim that a contract without a policy cannot be called ignores the explicit-payer path described immediately above. action::payer() returns the sysio.payer actor, and CPU/NET accounting checks that payer—not the called contract. A provisioned caller or relayer can therefore execute an otherwise unprovisioned contract, provided any RAM effects fit or are separately funded. Please qualify this as the behavior under default contract-pays billing; the same overstatement recurs around lines 140–149.
There was a problem hiding this comment.
Correct, and confirmed against the implementation. accounts_billing is keyed on act.payer() and nothing else, so an explicit sysio.payer leaves the called contract out of the map entirely and its limits are never consulted.
Verified with a throwaway test -- one contract deployed at net=0, cpu=0, called twice:
contract limits: ram=10485760 net=0 cpu=0
default billing -> account payloadless net usage is too high: 132 > 0
explicit payer -> executed, console: Im a payloadless action
alice cpu used 0 -> 2001
contract cpu used 0 -> 0
Same unprovisioned contract both times: default billing fails, {alice, sysio.payer} executes it, and the contract's usage and limits never move.
Both sites now scope the claim to default contract-pays billing and state the explicit-payer path. The one under "What happens with no policy at all" spells out the billing-map keying and ends on the distinction that matters: an unprovisioned contract is inert for ordinary users, not universally inert.
There was a problem hiding this comment.
Thanks—the revised sections now describe the explicit-payer path correctly. The full-document reread found the same universal claim still present in three other places, though:
- lines 125–126: “it is the payer for every call into it”
- lines 509–510: “the contract is the payer for every call into it” / users are never billed
- lines 585–586: “A contract either holds a policy or it is inert. That is the only distinction.”
All three need the same default-billing qualification. A provisioned explicit payer can drive a contract with zero CPU/NET, and a user who opts into sysio.payer is billed rather than the contract. The PR description also still says an unprovisioned contract’s zeros are fatal without mentioning this exception, so please update that summary to match the corrected document.
There was a problem hiding this comment.
Good catch, and the right criticism -- I fixed the two sites you raised and did not sweep the document for the same pattern.
All three are qualified in 5ad3d19e3e, plus a fourth I found doing the sweep you prompted:
- 125-126 -> "the payer for every call into it that does not name one explicitly"
- 509-510 -> scoped to ordinary users, noting a user is billed only by deliberately opting in with
sysio.payer - 585-586 -> the whitelist bullet no longer says a policy-less contract is simply inert; it is callable by a caller who names itself payer and covers the cost
- the DEX scenario carried the same "payer for every one of them" phrasing and is qualified the same way
The short-version wording I left alone: it says "normal transaction" and the paragraph immediately after states the explicit-payer exception, so it already reads correctly.
PR description updated to match -- it now says the zeros are fatal under default billing and are not consulted at all when an action names an explicit payer.
There was a problem hiding this comment.
One absolute version of this claim is still present in How this compares to EOS and Antelope: “On Wire the account that signs is not billed at all. The contract is.” The adjacent table likewise unconditionally lists “Called contract.” Both still exclude the explicit sysio.payer path documented elsewhere. Please qualify this paragraph and table as describing default/ordinary billing (for example, “By default…”); after that, this is ready from me.
There was a problem hiding this comment.
Fixed in 3943b7c9b3.
The paragraph now leads with the default and names the exception: "On Wire the signer is billed only if it asks to be. By default the contract is... A signer that names itself with sysio.payer takes the bill instead -- the exception, not the ordinary path."
The table row reads "Called contract, unless the action names an explicit payer".
Thanks for the three passes on this one. The pattern in all of them was the same on my side -- I verified the default billing path and then wrote as though it were the only path, so each fix addressed the site you pointed at rather than the claim.
| For `sysio.token`, transfers are RAM-free for both parties whether or not new state is created. | ||
| Alice is charged nothing, for anything, on a token transfer. | ||
|
|
||
| > This is a property of `sysio.token`'s implementation, not a universal rule. A third-party token |
There was a problem hiding this comment.
A third-party token cannot merely name from or to as its RAM payer. For a positive RAM delta billed outside the receiver, apply_context::validate_account_ram_deltas requires that account to appear with sysio.payer; the existing payer-choice test expects active-only authorization to fail with Missing sysio.payer. Adding that marker also makes the user the action’s CPU/NET payer. Please document this explicit opt-in rather than implying the contract can unilaterally charge RAM.
There was a problem hiding this comment.
Right on both counts, and this was the most wrong of the five -- the original text implied a contract could unilaterally name from/to as its RAM payer.
apply_context::validate_account_ram_deltas requires an account taking a positive RAM delta outside the receiver to appear with sysio.payer, and payer_choice_test asserts exactly that failure (Missing sysio.payer) for active-only authorization.
Your second point is the sharper one and I had missed it: because sysio.payer must sit at index 0, adding it also makes that user the action's CPU and NET payer. The note now says billing RAM to a user is an explicit user opt-in that carries bandwidth with it, not a contract-side choice, and keeps the recommendation to bill RAM to the contract account -- which needs no such marker.
| The contract-plus-ABI footprint is **under one tenth of a SYS**. For scale, even a *tier-3* node | ||
| owner — the smallest tier — holds ~1.93 SYS free, enough to sponsor several small contracts. | ||
|
|
||
| The provisioning is a single `addpolicy` on the contract account. Because the contract is the payer |
There was a problem hiding this comment.
The policy is granted once, but a token doing a million transfers does not necessarily need the same allocation as one doing ten. Transfers that create new holder rows consume persistent RAM, as the table immediately above demonstrates. Please clarify that CPU/NET are replenishing shares rather than per-transaction payments, while the policy’s RAM capacity must still cover state growth.
There was a problem hiding this comment.
Agreed -- the sentence contradicted the RAM table directly above it.
Rewritten as a split, since the two resources behave differently. CPU and NET meter rate rather than count, so volume determines whether the weight is a wide enough slice to sustain it -- exceeding it fails until the window rolls forward, rather than drawing down a balance. RAM is occupancy and accumulates with every new holder row, so a token expecting a million holders needs a policy sized for them, or a contract that bills those rows elsewhere.
| | End user needs native token | Yes | Yes | Yes | **No** | | ||
| | CPU/NET acquired by | Locking tokens | Renting from a pool | Daily fee on a curve | **A node owner's policy** | | ||
| | RAM acquired by | Bancor market purchase | same | same | **A node owner's policy** | | ||
| | Price set by | RAM market | Rental market | Utilization curve | **Not set on-chain** | |
There was a problem hiding this comment.
“Not set on-chain” conflicts with the on-chain activateroa/setbyteprice byte-conversion rate discussed later. If this row means the commercial terms between an issuer and recipient are off-chain, please label it accordingly; the RAM bytes-per-unit rate itself is explicitly set on-chain.
There was a problem hiding this comment.
Agreed -- the row was ambiguous, and bytes_per_unit is plainly set on-chain by activateroa / setbyteprice.
Relabelled to name what is actually off-chain: Acquisition price set by -> Off-chain, between issuer and recipient.
Addresses review on #583. Five corrections, all narrowing overstated claims. An unprovisioned contract is not universally uncallable. The billing map is keyed on payer() and nothing else, so an action carrying {caller, sysio.payer} leaves the contract out of the map entirely and its zero CPU/NET is never consulted. A provisioned caller or relayer can drive it, subject to its own capacity and any RAM the contract bills to itself. Both sites that claimed otherwise now scope the claim to default contract-pays billing. A contract cannot unilaterally bill RAM to its users. apply_context::validate_account_ram_deltas requires an account taking a positive RAM delta outside the receiver to appear with the sysio.payer permission, and since that marker must sit at index 0 it also makes the user the action's CPU and NET payer. Billing RAM to a user is therefore an explicit user opt-in that carries bandwidth with it, not a contract-side choice. setbyteprice is not a remedy for proportional-share dilution. It rewrites roastate.bytes_per_unit, which governs RAM weight to byte conversion for policies struck afterwards, and touches neither CPU/NET weights nor the total that divides them. The remedy for a shrinking slice is more weight. Transaction volume and policy sizing are not independent. CPU and NET meter rate rather than count, so volume determines whether the weight sustains the rate; RAM is occupancy and accumulates with every new row, so a token expecting many holders needs a policy sized for them. The comparison table's "price set by" row read as though no price is on-chain, which conflicts with the bytes-per-unit rate activateroa and setbyteprice set. It now names what is actually off-chain: the terms between issuer and recipient.
Follow-up on the same review point. The earlier fix corrected the two sites it was raised against but did not sweep the document, leaving the universal form of the claim in four more places. "It is the payer for every call into it" now reads "for every call into it that does not name one explicitly". The developer scenario's "users of that token are never billed" is scoped to ordinary users and notes that a user is billed only by deliberately opting in with sysio.payer. The whitelist bullet no longer says a contract without a policy is simply inert -- it is callable by a caller who names itself payer and covers the cost. The DEX scenario carried the same "payer for every one of them" phrasing and is qualified the same way. The short-version wording was already correct: it says "normal transaction" and the paragraph immediately after states the explicit-payer exception.
Last absolute form of the contract-pays claim. The paragraph said the signing account is not billed at all, and the table listed the called contract as the payer unconditionally; both excluded the explicit sysio.payer path. The paragraph now leads with the default and names the exception, and the table row reads "Called contract, unless the action names an explicit payer".
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed the complete current diff at 3943b7c. The default-billing language and explicit sysio.payer exception are now consistent throughout the document and PR description. No remaining issues found.
Round of external feedback on the published page, from a reader coming at it as a developer rather than a protocol author. Opens with what a reader actually wants to know -- a new account costs nothing and can use every provisioned contract immediately -- then builds outward to who does pay. The clearest statement of that was two thirds of the way down; the top led with the payer() rule instead, which is mechanism before outcome. Reorders the payer section to state the default first and the sysio.payer exception second, with its three requirements in the same breath rather than ninety lines later. Read alone, opening on the rule sounded like adding a permission name is enough to obtain resources, which it is not. Adds a mermaid flowchart of the same decision. Adds a porting section. A contract carried over from Antelope works on deployment there because callers arrive with their own staked or rented bandwidth; on Wire they hold zero, so the same contract fails on its first call until it holds a policy. That is a plausible way to lose a developer and the document did not mention it. Adds CPU and NET sizing guidance built on measurement rather than a made-up constant: read cpu_usage_us and the action's billable size from a testnet trace, size for a rate rather than a total, and budget headroom for retries. NET is anchored on measured values (132 bytes for a minimal action, 148 with a second authorization). No CPU figure is given -- it depends on contract, hardware, and runtime, and a number from elsewhere would be worse than none. Corrects the comparison intro. "EOS, Antelope, or Vaulta" implied three separate lineages; Vaulta is the EOS Network line, and no single chain ran staking, REX, and PowerUp as a set. Explains -1 as the sentinel it is -- tested with `cpu_weight < 0` and `is_unlimited_cpu`, with a large finite value substituted where a number is needed. It is neither a very large number nor an overflow, and readers who see -1 in a get_account response need the right model. Frames Wire's RAM billing against Antelope's: there a contract names a RAM payer and needs only that the account authorized the action, with bandwidth on a separate path; here the sysio.payer marker binds the two, so a user charged for storage is charged for bandwidth too. Replaces the staking bullet with a narrower one about delegatebw. Saying staking exists "for other purposes" without naming them raised the question it meant to close, and answering it properly belongs in a document about staking.
huangminghuang
left a comment
There was a problem hiding this comment.
Inline documentation-correctness findings from a review of the current PR head.
|
|
||
| | Item | Bytes | `ram_weight` | | ||
| |---|---|---| | ||
| | A 60 KB contract WASM | ~61,440 | ~0.0591 SYS | |
There was a problem hiding this comment.
[P1] Apply the 10x WASM RAM multiplier
apply_sysio_setcode bills code_size * setcode_ram_bytes_multiplier, and that multiplier is 10. A 60 KiB WASM therefore consumes about 614,400 bytes / 0.5908 SYS before metadata and ABI, not 61,440 bytes / 0.0591 SYS. The later claim that contract plus ABI is under 0.1 SYS is consequently false and can cause a developer to underprovision deployment.
There was a problem hiding this comment.
Done in a2287bd911. Confirmed and worse than the table implied.
config.hpp sets setcode_ram_bytes_multiplier = 10, and apply_sysio_setcode bills new_size = code_size * multiplier, so the 60 KiB example needed ~614,400 bytes / 0.5908 SYS — your figures. Corrected the row, and replaced "under one tenth of a SYS" with ~0.6 SYS for contract plus ABI.
Three things I checked while fixing it:
setabiis billed 1x —new_size = abi_size, no multiplier — so the ABI row was already right. The callout says the ×10 is code-only so nobody scales the ABI too.- A one-time 152-byte
account_metadata_objectrow is billed alongside, when the account has none yet. Added as its own row. - The charge is levied even when the WASM is byte-identical to one already deployed (the
code_objectis shared by refcount, but the deployer still pays the full ×10), and a redeploy subtractsold_size * 10first, so an upgrade costs only the difference. Both are in the callout.
| Nothing is wrong with the contract, the transaction, or the signer. The contract has no policy, and | ||
| the contract is the payer. It will look dead until a node owner issues it one. | ||
|
|
||
| The fix is not to change the contract or ask users to acquire resources. It is a single `addpolicy` |
There was a problem hiding this comment.
[P2] Provisioning alone does not make every port work
A common Antelope contract bills newly created rows to an authorizing user. Wire rejects the unchanged {user, active} transaction with Missing sysio.payer, regardless of the contract's policy. Such a port must bill RAM to the contract or change the authorization and provision the user, so the promise that the same unmodified transaction succeeds is unsafe.
There was a problem hiding this comment.
Done in a2287bd911. This was the most useful of the batch — the section promised something the chain does not deliver.
Confirmed: {user, "active"_n} with user passed to emplace gives payer != receiver with a positive delta on an unprivileged contract, so validate_account_ram_deltas demands sysio.payer and the transaction dies with Requested payer alice did not authorize payment. Missing sysio.payer — the contract's policy is irrelevant.
Split the claim in two: a policy makes an unprovisioned contract callable, and where the contract bills its RAM is a separate, mandatory porting decision. Both routes are spelled out — bill rows to the contract (emplace(get_self(), ...), the gasless path, caller's transaction unchanged), or keep billing the user and have the client add sysio.payer at index 0, which makes them the CPU/NET payer too.
| what you have used depends on which chain and which era. | ||
|
|
||
| **Staking (2018).** You locked tokens with `delegatebw` to get a proportional share of CPU and NET, | ||
| and bought RAM outright from a Bancor-curve market with `buyram`. Users had to hold enough token to |
There was a problem hiding this comment.
[P2] Correct the Antelope sponsorship comparison
Antelope does not require the resource recipient to own tokens: delegatebw separates from and receiver, while PowerUp separates fee payer and resource receiver. PowerUp also maintains state, orders, and an expiry queue, so it is not stateless. See the official delegation guide and PowerUp guide.
There was a problem hiding this comment.
Done in a2287bd911. Both corrections taken; the paragraph was drawing the contrast in the wrong place.
delegatebw does separate from and receiver, and PowerUp separates the fee payer from the resource receiver, so neither requires the recipient to hold tokens. And PowerUp is not stateless — it keeps a powup_state singleton whose utilization drives the price, plus an order table that expires as rentals lapse. Dropped "stateless" and rewrote both paragraphs.
The real contrast is not "user needs tokens" but who pays, and how often: on those chains somebody funds each user, per user, and on PowerUp per user per day. On Wire one policy on the contract covers every caller. Updated the two table rows that carried the old framing — End user needs native token now reads "No — but someone does, per user", and Cost to onboard a user likewise. Better contrast and it survives scrutiny.
| For `sysio.token`, transfers are RAM-free for both parties whether or not new state is created. | ||
| Alice is charged nothing, for anything, on a token transfer. | ||
|
|
||
| > This is a property of `sysio.token`'s implementation, not a universal rule — but a contract |
There was a problem hiding this comment.
[P2] Scope the RAM opt-in guarantee
validate_account_ram_deltas explicitly bypasses sysio.payer when the receiver is a privileged sysio.* contract; privileged_kv_payer_bypass proves such a contract can bill a user's RAM without that user's authorization. Scope this guarantee to unprivileged third-party contracts.
There was a problem hiding this comment.
Done in a2287bd911. Scoped to unprivileged contracts, and the bypass is narrower than "privileged".
Reading validate_account_ram_deltas, it is privileged AND a sysio. name prefix — neither alone. privileged_kv_payer_bypass is precisely that pin: sysio.test (privileged, prefixed) bills alice's RAM with no marker and succeeds, while privtest — equally privileged, not prefixed — still fails with Missing sysio.payer. There is a third bypass I added too: any authorization whose actor is sysio itself, regardless of permission or privilege.
Since privilege is granted by governance, the section now frames this as a statement about the system contracts rather than an escape hatch an application can reach.
| ### The contract's own share is a hard cap | ||
|
|
||
| CPU and NET are rate limits over a window, and the payer's limit is the one that applies. Spam | ||
| aimed at a contract consumes that contract's share and nothing else. Once it is exhausted, further |
There was a problem hiding this comment.
[P2] Contract quotas do not eliminate shared-block impact
Successful transactions consume both the payer's account window and block-wide pending CPU/NET capacity. Spam remains bounded by the contract's quota, but a highly provisioned contract can consume block capacity and delay unrelated traffic; the blast radius is therefore not categorically limited to that contract.
There was a problem hiding this comment.
Done in a2287bd911. Agreed — the paragraph overclaimed.
add_transaction_usage does both halves: the payer's rolling window, and rls.pending_cpu_usage += total_cpu_usage; rls.pending_net_usage += total_net_usage; at block level, asserted against the block maxima. pending_*_usage is what get_block_cpu_limit() / get_block_net_limit() subtract from to size the next transaction's limits, so the drain is unconditional.
Kept the structural point — the spammer cannot consume another contract's quota — but replaced "cannot spill onto ... the rest of the network" with what the quota actually guarantees: the spam stops when that contract's share runs out, not that nobody else notices while it lasts. Bounding the shared-capacity effect is the second layer's job.
| transfer_ram( get_self(), new_account_name, sysiosystem::newaccount_ram ); // 1144 bytes | ||
| ``` | ||
|
|
||
| For a **user** account, those zeros are never consulted. It signs, the contract pays, the |
There was a problem hiding this comment.
[P2] A signer's zero limit can be consulted subjectively
With subjective billing enabled, verify_init_subjective_billing calls get_cpu_limit for first authorizers that are not payers and includes that objective limit in the available-budget calculation. Scope never consulted to objective/default billing; the later subjective-billing section already documents this exception.
There was a problem hiding this comment.
Done in a2287bd911. Scoped to default billing, with one nuance added.
verify_init_subjective_billing does take first authorizers that are not payers and call get_cpu_limit on each. But it folds the objective limit in additively — available = subjective_cpu_allowed + cpu_limit - subjective_bill — and skips the account entirely when unlimited. So a zero objective limit contributes nothing rather than failing the transaction: the signer transacts on the node's subjective allowance alone until its accumulated bill exhausts that.
Worth stating explicitly, since "the zeros are consulted" could otherwise read as "an unprovisioned user is rejected", which is the opposite of what happens. The note cross-references the subjective-billing section and repeats that it is off by default.
| - **Successful transactions do not accumulate.** A subjective bill is held as `pending_cpu_us` and | ||
| removed when the transaction appears in a block. Traffic that lands costs the signer nothing over | ||
| time. | ||
| - **Failures accumulate and decay slowly.** A transaction that fails or expires moves its bill into |
There was a problem hiding this comment.
[P2] Not every subjective failure accumulates
subjective_bill_failure excludes resource-exhaustion exceptions and duplicate transactions, and producer failure tracking excludes those paths too. Qualify this as non-exhaustion, non-duplicate failures; expired pending transactions are handled separately.
There was a problem hiding this comment.
Done in a2287bd911, with one addition worth flagging.
The gate is in transaction_context, not the producer plugin: subjective billing is skipped when exception_is_exhausted(e) || e.code() == tx_duplicate::code_value. exception_is_exhausted covers block_cpu_usage_exceeded, block_net_usage_exceeded, deadline_exception, interrupt_exception, and the read-only VM-OC compile failure — all block-level.
The addition: tx_cpu_usage_exceeded and tx_net_usage_exceeded are not excluded. Blowing your own limit does accumulate. That is the distinction a reader most needs, so the doc now states both halves. Also noted that the producer's separate failure counter skips the same two classes, and that subjective billing is CPU-only — subjective_bill_failure receives the full billing struct but only ever consumes cpu_usage_us.
There was a problem hiding this comment.
Correction to my reply above, in cac1eba19a: I dropped the "subjective billing is CPU-only" line I said I had added. The exclusion list — the part of this finding a reader acts on — is unchanged.
The fact is true, but stated bare it raises a question it does not answer: if there is no subjective NET, how does a user with zero NET transact at all? The answer is that objective billing keys accounts_billing on act.payer() for NET exactly as for CPU, so the signer is unbilled in both currencies and there is nothing for a subjective NET meter to do — signer-driven NET pressure is bounded by the contract's objective quota, and converts into tx_net_usage_exceeded failures, which the exclusion list above confirms are billed and do count toward subjective-account-max-failures.
That is a paragraph, and the page is already long. Removed rather than expanded.
|
|
||
| ### Why the contract can afford it | ||
|
|
||
| System accounts — anything whose name prefix is `sysio` — carry unlimited resource limits, and |
There was a problem hiding this comment.
[P2] A sysio prefix does not guarantee unlimited limits
activateroa deliberately initializes sysio.acct with zero CPU and NET, so the prefix alone does not confer -1 limits. Since ROA also refuses CPU/NET grants to sysio.*, scope this statement to the standard executable system contracts that were explicitly provisioned as unlimited.
There was a problem hiding this comment.
Done in a2287bd911. The claim was true in effect and wrong about the mechanism, and sysio.acct is the counterexample.
There is no chain-level rule that a sysio. prefix means unlimited. The system contracts are unlimited for two unrelated reasons: a resource-limits row is born -1 (net_weight, cpu_weight, ram_bytes all default to it) and those accounts were simply never provisioned; and separately sysio.roa's add_system_resources passes -1, -1 for a sysio.-prefixed owner while addpolicy/expandpolicy refuse CPU/NET to one.
And activateroa gives sysio.acct an explicit 0, 0 — it is a pure RAM bucket for account creation. Quoted it as the counterexample and reframed the guarantee around the contracts that were provisioned unlimited.
| | 2 | 0.15% | 84 | 12.6% | | ||
| | 3 | 0.003% | 1,000 | 3.0% | | ||
|
|
||
| Registration consumes part of an owner's own budget — a personal RAM allocation, 10% of the tier |
There was a problem hiding this comment.
[P2] Reconcile concurrent PR #585
These figures match current master, but open PR #585 removes the personal RAM/CPU/NET policy for tiers 2 and 3. Once both PRs land, only tier 1 receives the flat personal grant; the tier-2 and tier-3 free budgets become approximately 101.9196 SYS and 2.0385 SYS. One of the PRs needs to own this documentation update.
There was a problem hiding this comment.
Done in a2287bd911. This PR takes ownership of it.
Verified both states against the contract. On master the personal policy is flat across tiers — RAM 0.0080 + NET 0.0500 + CPU 0.0500 = 0.1080 SYS — giving free budgets of 2717.7480 / 101.8116 / 1.9305 SYS, which is what the table has. Under #585 the personal policy becomes tier-1 only while the 10% network-RAM deposit stays untiered, so tiers 2 and 3 keep that 0.1080: 101.9196 and 2.0385 SYS, tier 1 unchanged. Your figures exactly.
Added a "Pending change" note carrying both numbers and stating that whichever PR lands second updates the table, plus a pointer from What registration provisions — that section already explains the flat-vs-tiered disparity (4.77% of a tier-3 allocation, 0.0036% of a tier-1's), which is precisely what #585 addresses, so the two now read together.
If #585 merges first I will rebase and collapse the note into the table; if this lands first the note tells #585 exactly what to change.
| **Applications get paid for by node owners.** A contract cannot conjure its own capacity. It | ||
| receives a **policy** — a grant of CPU, NET, and RAM weight — from a **node owner**, who holds a | ||
| fixed share of the network's capacity determined by their tier and issues slices of it through the | ||
| `sysio.roa` contract. Only registered node owners can issue policies. No account can grant itself |
There was a problem hiding this comment.
[P3] Node owners can grant themselves bandwidth
addpolicy permits owner == issuer, and node-owner registration itself creates such a self-issued policy. The intended restriction is that a non-node-owner cannot grant itself bandwidth.
There was a problem hiding this comment.
Done in a2287bd911. You are right, and the doc contradicted itself two sections later.
addpolicy has no require_auth(owner) at all — only require_auth(issuer) — and registration itself writes a policy with issuer == owner, which What registration provisions already described as "a self-issued policy". So the blanket sentence was wrong on both counts.
Rewrote it as: only registered node owners can issue, and only out of their own tier budget, so an ordinary account cannot grant itself bandwidth and no account can create capacity that was not already allocated to some node owner — then stated plainly that an owner may name itself, as registration does.
(Structurally addpolicy(self, self) currently hits the duplicate-policy guard, since registration already occupies that scope/key, so a node owner self-expands via expandpolicy today. Under #585 tiers 2 and 3 no longer get that row and addpolicy becomes the live path — another reason the two PRs want reconciling.)
Each item below was checked against the implementation before the text changed;
the corrections are grouped by what they turn on.
RAM billing
* setcode bills `code_size * setcode_ram_bytes_multiplier`, and that multiplier
is 10 -- so the 60 KiB deployment example needed ~614,400 bytes / 0.5908 SYS,
not ~61,440 / 0.0591, and the contract-plus-ABI footprint is ~0.6 SYS rather
than "under one tenth". Sizing from the unmultiplied figure would have failed
the setcode. `setabi` is billed 1x, so the ABI row was already right; the
one-time 152-byte account_metadata row was missing and is now listed.
* The `sysio.payer` RAM guarantee holds for unprivileged contracts only.
`validate_account_ram_deltas` bypasses it when the receiver is privileged AND
`sysio.`-prefixed -- `privileged_kv_payer_bypass` pins that both are required,
since equally-privileged `privtest` still fails -- and again for a `sysio`
payer, or any authorization whose actor is `sysio`.
* A ported Antelope contract that bills rows to `{user, active}` is rejected
whatever policy it holds. Provisioning the contract makes it callable; where
it bills its RAM is a separate and mandatory porting decision.
* A sponsorship row is 144 bytes (16 key + 16 value + 112), not 296: #584
dropped the `byusername` index, whose row cost another 136. The
billed-to-the-contract counterfactual is therefore ~1.09M users, not 530,000.
Neither RAM pool is a ceiling -- `addpolicy` refuses CPU/NET to a `sysio.`
account but not RAM.
CPU/NET billing
* Failed transactions do not consume objective CPU/NET. `add_transaction_usage`
is reached only from `finalize()`, which a throwing `exec()` never reaches,
and the session is undone. A retry that lands is billed once.
* A successful transaction drains block-wide `pending_cpu_usage` /
`pending_net_usage` as well as the payer's window, so a contract's quota
bounds the spam without making the blast radius categorically local.
* Subjective billing excludes `tx_duplicate` and block-level exhaustion, but
not `tx_cpu_usage_exceeded` / `tx_net_usage_exceeded`; it is CPU-only.
* A signer's zero limit is consulted once subjective billing is on --
`verify_init_subjective_billing` folds `get_cpu_limit` in additively -- so it
contributes nothing rather than failing the transaction. "Never consulted"
now says "under default billing".
Authorization
* The permission paired with `sysio.payer` need not be `active`.
`authorization_manager` accepts any entry whose actor matches the payer and
whose permission is not `sysio.payer` itself.
* A `sysio.` prefix does not confer unlimited limits. The system contracts are
unlimited because a resource-limits row is born `-1` and they were never
provisioned; `activateroa` gives `sysio.acct` an explicit 0/0.
* A node owner can self-issue: `addpolicy` has no `require_auth(owner)` and
registration itself writes a policy with `issuer == owner`. Scoped the claim
to ordinary accounts.
Policy lifecycle and neighbours
* `reducepolicy` unwinds CPU and NET fully but caps RAM at the owner's unused
quota, floored to `bytes_per_unit`, decrementing by what was reclaimed rather
than requested -- so consumed RAM pins residual weight in the issuer's budget.
`ram_unused` is account-wide, so co-issuers draw on one shared pool.
* Tier figures track master, with #585's tier-1-only personal policy and the
resulting 101.9196 / 2.0385 SYS budgets noted as pending.
* Antelope sponsorship: `delegatebw` separates `from`/`receiver` and PowerUp
separates fee `payer`/`receiver`, so neither requires the recipient to hold
tokens; PowerUp is not stateless, keeping `powup_state` and an order-expiry
queue. The honest contrast is per-user funding versus one policy per contract.
Also fixed two internal anchors; every link in the file now resolves.
Change-Id: I9bcee3327fc6e9c43aeed6cbdd3a95ab6bf6cc7a
It stated a fact without the context that makes it meaningful -- objective billing keys on the payer for NET as well as CPU, so the signer is unbilled either way and there is nothing for a subjective NET meter to do. Explaining that costs a paragraph the page does not have room for; stating it bare invites the question instead of answering it. The exclusion list above is the part a reader acts on. Change-Id: I2f8489a428b37772015e6efa4de4e70ddf0c62ad
huangminghuang
left a comment
There was a problem hiding this comment.
Inline findings from the re-review of the current PR head.
| - the **same actor** also appears on that action under a real permission, and | ||
| - the transaction carries **signatures** satisfying that paired declared authorization. | ||
|
|
||
| The paired permission is not required to be `active`. `authorization_manager` looks for any entry |
There was a problem hiding this comment.
[P2] Document the get_required_keys limitation
Consensus accepts any paired non-sysio.payer permission, but authorization_manager::get_required_keys still hard-codes <payer>@active when processing the virtual payer entry. An owner/custom-only key can therefore fail standard signing-tool discovery even though direct submission could authorize. Either fix that implementation mismatch or qualify the claim that owner/custom permissions work end-to-end.
There was a problem hiding this comment.
Fixed in the implementation rather than qualified here — #590, targeting master, to land before this.
You are right and it is worse than a doc problem. get_required_keys hard-codes <payer>@active while check_authorization pairs on auth.permission != sysio_payer_name, so a self-pay transaction under owner or a linked custom permission is accepted by consensus and rejected by the endpoint every signing tool calls. It stayed hidden because a fresh account usually carries the same key on owner and active.
The fix removes the special case entirely instead of mirroring the pairing rule: the marker is virtual — no permission_object, no keys — and consensus already requires it to be paired with a real permission, which is checked on its own iteration of the same loop. Skipping it makes discovery agree with consensus by construction rather than by keeping a second copy in sync. Non-consensus path (read-only RPC plus the snapshot provider self-signing), so no protocol feature.
Covered by get_required_keys_explicit_payer_tests on payloadless::doit, asserting for all three pairings both the discovered key set and that a transaction signed with exactly it is accepted — agreement is the property, and either half alone passed before. Verified as a guard (reinstating @active fails the owner and custom cases); full unit_test: 1516 cases green.
This page is unchanged and now assumes #590.
| B -->|"Yes — opt-in"| D{"Same actor also present<br/>with a real permission,<br/>and signed for?"} | ||
| D -->|"No"| E["Rejected:<br/>unsatisfied authorization"] | ||
| D -->|"Yes"| F["Payer = that actor"] | ||
| C --> G["Contract needs a policy.<br/>Signer never checked."] |
There was a problem hiding this comment.
[P2] Qualify the remaining signer checks
The flowchart and lines 156–157 still say a non-payer signer is never checked. That is only true for objective billing; when subjective billing is enabled, verify_init_subjective_billing checks non-payer first authorizers and reads their CPU limits. The new caveat at lines 253–263 now contradicts these absolutes.
There was a problem hiding this comment.
Done in e365f2e2f7 — by deleting, not qualifying. You caught that my own round-2 caveat had created the contradiction.
The flowchart now says "Signer not billed", and the paragraph reads "The objective billing map is keyed on payer()… consensus neither charges nor limits its CPU and NET", with one clause noting a producer running subjective billing does meter the signer.
Then the 10-line caveat I added at 253-263 is gone. The subjective-billing section already carries the additive-budget formula in full, so that block was pure duplication — which is what let it drift out of agreement with the text above it in the first place.
|
|
||
| | Pool | Size | Funds | | ||
| |---|---|---| | ||
| | `sysio.roa` | ~157 MB, fixed | The contract's own rows: policies, reslimits, node-owner records | |
There was a problem hiding this comment.
[P2] Do not label the initial ROA pool fixed
This table still calls the approximately 157 MB sysio.roa allocation fixed, while lines 421–423 correctly explain that RAM-only policies can extend it. Describe it as the activation allocation, perhaps noting that registration does not automatically grow it.
There was a problem hiding this comment.
Done in e365f2e2f7. Table now reads "~157 MB at activation".
Also took the second half explicitly: the paragraph below says neither pool is a ceiling, since addpolicy refuses CPU and NET to a sysio.-prefixed account but not RAM, so a node owner can extend either with a RAM-only policy.
| ``` | ||
|
|
||
| This is how a contract gets provisioned by more than one sponsor: several node owners co-sponsor | ||
| the account, each on their own terms, each able to enter or exit without disturbing the others. |
There was a problem hiding this comment.
[P2] A co-sponsor cannot always exit
This still promises every issuer can exit, but consumed account-wide RAM can pin residual policy weight and issuer budget. The corrected explanation at lines 856–877 explicitly documents that limitation; use reduce its own policy without modifying another issuer's row instead of guaranteeing exit.
There was a problem hiding this comment.
Done in e365f2e2f7, using your wording — "each able to reduce its own policy without touching another issuer's row".
The duplication was the real problem: the limitation was stated in two places, which is how one of them stayed wrong. The full treatment now lives in the Stacking policies bullet, where a multi-issuer reader actually meets it, and covers the cap at unused RAM, the bytes_per_unit floor, decrementing by reclaimed-not-requested, and the account-wide pool. Capacity distribution points at it in one clause instead of restating it in 22 lines.
| | 2 | 113.2440 SYS | ~101.81 SYS | ~106 MB | | ||
| | 3 | 2.2649 SYS | ~1.93 SYS | ~2.0 MB | | ||
|
|
||
| > **Pending change.** [#585](https://github.com/Wire-Network/wire-sysio/pull/585) provisions the |
There was a problem hiding this comment.
[P2] Make the #585 integration concrete
The note promises that whichever PR lands second will update these figures, but current #585 does not modify this file. Merging both unchanged leaves the all-tier personal-policy text and line 740's 0.0500 SYS statement stale. Add the post-#585 documentation commit to one branch and enforce the merge order.
There was a problem hiding this comment.
Done in e365f2e2f7 — with a different fix than merge-order coordination, which I think is more robust.
You are right that neither PR modifies the other's surface and that ordering them is fragile. But the whole delta is 0.1080 SYS, and stating the free budgets one significant figure coarser makes the table correct under both: ~2,718 / ~102 / ~2 SYS holds on master (2717.7480 / 101.8116 / 1.9305) and after #585 (unchanged / 101.9196 / 2.0385).
So the pending-change block is gone, neither PR has to chase the other, and there is no merge order to enforce. #585's qualitative change is noted where the grant is described — one clause, no numbers to go stale — and line 740 now reads "the personal allocation a tier-1 node owner's registration creates".
| of leaving it idle. It improved capital efficiency and added a third system to understand alongside | ||
| staking and the RAM market. | ||
|
|
||
| **PowerUp (2021).** Replaced REX rentals with a daily-expiring rental priced off a utilization |
There was a problem hiding this comment.
[P3] PowerUp duration is configurable
PowerUp is not inherently daily: powerup_days is configuration state and each action's days must match it. The reference-contract default is 30 days, although a chain can configure one day. Replace the daily-expiring/per-day absolutes with configured-term wording. See the official PowerUp guide.
There was a problem hiding this comment.
Done in e365f2e2f7. "Daily" is now "at term", and the paragraph says the term is configuration — powerup_days, 30 in the reference contract, with each order required to match — not inherently daily.
Swept the three comparison-table cells that carried the old framing too: CPU/NET acquired by → "Term fee on a curve", Reclaimable by → "Expires at term", Cost to onboard a user → "Per-user recurring fee".
| max_use_in_window = virtual_capacity_in_window × your_weight / total_weight_across_all_accounts | ||
| ``` | ||
|
|
||
| The denominator is the sum of every account's positive CPU (or NET) weight — that is, the sum of |
There was a problem hiding this comment.
[P3] The denominator is not structurally ROA-only
The denominator is resource_limits_state.total_cpu_weight or total_net_weight: every positive account limit. Privileged setalimits, setacctcpu, and setacctnet can add positive weights outside ROA. Qualify the ROA equivalence as the intended normal production state.
There was a problem hiding this comment.
Done in e365f2e2f7. It now names the actual field and marks the equivalence as a production property rather than a structural one:
The denominator is
resource_limits_state'stotal_cpu_weight(ortotal_net_weight): the sum of every account's positive weight, whatever set it. […] In normal production every positive weight comes from a ROA policy, so the two are the same number — but privilegedsetalimits/setacctcpu/setacctnetcan add weight outside ROA, and bootstrap does.
| | `Not enough unallocated SYS for this policy.` | Issuer's budget is exhausted | | ||
| | `A policy for this owner already exists from this issuer.` | Use `expandpolicy` | | ||
| | `Cannot reduce policy before time_block` | The policy's committed term has not elapsed | | ||
| | `Cannot allocate CPU/NET to sysio accounts.` | System accounts keep unlimited CPU/NET by design | |
There was a problem hiding this comment.
[P3] Correct the sysio-account error explanation
This error is raised for every sysio.* policy recipient, including sysio.acct, which activateroa initializes at zero CPU/NET. The message means ROA refuses CPU/NET policies for the prefix; it does not establish that every such account is unlimited.
There was a problem hiding this comment.
Done in e365f2e2f7. The error row now reads: ROA refuses CPU/NET policies for any sysio.-prefixed owner — and explicitly, that this does not mean the account is unlimited, with sysio.acct named as the counterexample.
This is the same confusion the round-2 fix removed from Why the contract can afford it; it had survived in the error table.
| ## The short version | ||
|
|
||
| **A new account on Wire costs its owner nothing and can immediately use every provisioned | ||
| contract on the network.** It holds no CPU or NET allocation, does not need one, and never |
There was a problem hiding this comment.
[P3] A user may later acquire a policy
Any existing account can later receive addpolicy capacity or become provisioned for explicit self-pay, so never acquires one remains too categorical. Need not acquire one for ordinary contract-paid calls captures the intended default.
There was a problem hiding this comment.
Done in e365f2e2f7, close to your wording: "holds no CPU or NET allocation and needs none for ordinary contract-paid calls". Dropped "never acquires one" — an account can certainly be granted a policy later, and one that opts into sysio.payer needs to be.
|
|
||
| ## Reference | ||
|
|
||
| ### Actions on `sysio.roa` |
There was a problem hiding this comment.
[P3] Complete the ROA reference tables
The current ABI also exposes giftram, setsyscode, and setsysabi; the tables section omits nodeownerreg, sponsors, and sponsorcount. Either include them or label these sections as selected public actions/tables rather than a complete reference.
There was a problem hiding this comment.
Done in e365f2e2f7 — labelled rather than expanded, since the page is already long and these are the allocation surface a reader comes here for.
Headings are now Selected actions on sysio.roa and Selected tables, with a line saying giftram, setsyscode and setsysabi are bootstrap/governance actions and omitted. nodeownerreg, sponsors and sponsorcount are likewise out of the selection.
Thirteen findings, several of which were the same claim stated in more than one
place -- so where the earlier text was already corrected further down, the
duplicate is deleted rather than qualified again. 937 -> 869 lines.
Corrections
* NET is not just the serialized action. `get_action_billable_size` adds a
share of the transaction's overhead -- fixed 16 bytes, signatures,
extensions, header -- apportioned across the actions. So signatures cost
NET, and batching amortizes the overhead. Both change how a developer sizes.
* The throughput denominator is `total_cpu_weight` / `total_net_weight`: every
positive weight, not structurally ROA's. Privileged `setalimits` /
`setacctcpu` / `setacctnet` can add weight outside ROA, and bootstrap does.
* Capacity is not policy-only: the 1,144-byte account gift, `giftram`, and the
privileged setters are separate routes, none open to applications.
* ROA has no consensus whitelist, but `nodeop` does -- `actor-whitelist` /
`contract-whitelist` are node-local and can refuse a provisioned contract.
* The `sysio` policy error means ROA refuses CPU/NET for the prefix; it does
not mean such accounts are unlimited, and `sysio.acct` is at zero.
* PowerUp's term is `powerup_days` (30 in the reference contract), not
inherently daily.
* A user "never acquires" a policy overstated it -- they need none for
ordinary contract-paid calls, but can be granted one.
* The ~157 MB `sysio.roa` allocation is where activation starts it, not fixed.
* Reference actions/tables are labelled as selected rather than complete.
Deduplicated
* The signer-is-never-checked absolutes now say "objective", and the caveat
added last round is deleted -- the subjective-billing section already
carries the additive-budget explanation in full.
* The reclaim limitation was stated twice. The full treatment now lives with
the stacking bullets, where a multi-issuer reader meets it; Capacity
distribution points at it and no longer promises a clean exit.
* The setcode and RAM-bypass callouts added last round are cut to the fact and
the test that pins it.
Tier figures
Stated at a precision correct both on master and under #585 (~2,718 / ~102 /
~2 SYS), so neither PR has to chase the other and the pending-change block is
gone. #585's tier-1-only change is noted where the grant is described, without
numbers that would go stale.
Depends on the get_required_keys fix
The claim that a `sysio.payer` entry works when paired with `owner` or a custom
permission is true at consensus but was not discoverable through
/v1/chain/get_required_keys. That is fixed separately and lands first; this
page assumes it.
Change-Id: Ic6ca4e47c11eeb8c6398d24f249667e187723d93
|
Round 3 addressed in #590 — This PR is now 869 lines, down from 937, with all thirteen findings addressed. Most of the reduction came from what the review exposed: three of the findings were an older absolute contradicting a caveat added in round 2, which is a redundancy signal — so those were deleted rather than qualified twice. Two calls worth flagging: NET sizing was substantively wrong and grew slightly (signatures cost NET, batching amortizes overhead — both affect how a developer sizes); and the #585 staleness is solved by stating the tier budgets one significant figure coarser, so they hold both before and after it and no merge order needs enforcing. |
huangminghuang
left a comment
There was a problem hiding this comment.
Seven inline findings from the latest re-review. Please also consolidate the PR description: its overview still says the throughput denominator is total ROA-allocated weight, contradicting the corrected document, and its round-2/round-3 sections give incompatible accounts of who owns the #585 update. The documented owner/custom payer flow still depends on open #590, so this PR should merge after it.
| | 2 | 0.15% | 84 | 12.6% | | ||
| | 3 | 0.003% | 1,000 | 3.0% | | ||
|
|
||
| Registration consumes part of an owner's own budget — 10% of the tier allocation set aside into |
There was a problem hiding this comment.
[P2] Keep tier documentation valid after #585
Coarsening the numeric budgets only makes part of this section stable. Lines 297–320 and 326–329 still state that every tier receives the same 0.1080 SYS self-issued personal policy, but #585 provisions it for tier 1 only and contains no documentation change. After #585, the tier-3 all-RAM figure is about 2.1 MB rather than line 306's 2.0 MB. Update one branch to describe the final behavior and enforce the merge order.
There was a problem hiding this comment.
Fixed in d2732ed1c5 by making the dependency explicit rather than pre-documenting unlanded behaviour. The tier section now carries a note that it describes current master, that #585 provisions the personal policy for tier 1 only and returns 0.1080 SYS to every tier-2 and tier-3 budget (tier 3 ~2.04 SYS, ~2.1 MB all-RAM), and that #585 merges after this PR and carries the corresponding edit. #585 is already MERGEABLE and blocked only on this one landing.
| For scale on the weight side: the personal allocation a tier-1 node owner's registration creates | ||
| is `0.0500 SYS` of each, and a routine test account is provisioned with `0.0010 SYS` of each. | ||
|
|
||
| Budget headroom for peaks above the average rate, not for failures: objective CPU and NET are |
There was a problem hiding this comment.
[P2] Account for subjective payer billing on failures
This is true only for objective accounting. update_billed_cpu_time adds CPU to accounts_billing[payer], and non-exhaustion failures pass that map to subjective_bill_failure; disable-subjective-payer-billing defaults to false. With subjective billing enabled, failed spam can therefore consume the contract payer's node-local CPU budget and, where different, the signer's. Qualify the no-charge statement as objective and include payer-side subjective headroom.
There was a problem hiding this comment.
Fixed in d2732ed1c5. Qualified to objective accounting, and the payer-side subjective cost is now stated: update_billed_cpu_time records CPU against the payer, non-exhaustion failures pass that map to subjective_bill_failure, and disable-subjective-payer-billing defaults to false -- so failed spam does eat the contract's node-local headroom. The headroom advice now says to leave room for it.
| Two limits worth knowing: | ||
|
|
||
| - `newuser` is **tier-1 only**. Tiers 2 and 3 cannot use it. | ||
| - Nobody can call the native `newaccount` directly. Wire gates it on the creator being a privileged |
There was a problem hiding this comment.
[P2] Native newaccount is available to privileged creators
apply_sysio_newaccount accepts this action whenever the declared creator authorized it and its account metadata is privileged. Ordinary accounts cannot call it, but an authorized privileged creator can; the tests exercise that path directly. Replace Nobody with the unprivileged/ordinary scope, and do not make the line-860 error guidance unconditionally recommend tier-1-only newuser.
There was a problem hiding this comment.
Fixed in d2732ed1c5. Scoped to ordinary accounts -- apply_sysio_newaccount accepts the action whenever the declared creator authorized it and is privileged, so a privileged creator can. The line now points applications at sysio.roa rather than asserting nobody can call it.
|
|
||
| ### Why the contract can afford it | ||
|
|
||
| The executable system contracts — `sysio.token`, `sysio.msig`, `sysio.wrap` and the rest — carry |
There was a problem hiding this comment.
[P2] Correct the system-account resource lifecycle
Production creates accounts such as sysio.token, sysio.msig, and sysio.wrap after sysio.system and ROA activation: native newaccount first writes 0/0/0 and transfers finite RAM, then setsyscode → giftram explicitly writes CPU/NET as -1 while funding the code RAM. These accounts do not simply retain limits that were born unlimited. Keep the correct conclusion that their CPU/NET is unlimited, but describe how the current bootstrap actually establishes it.
There was a problem hiding this comment.
Fixed in d2732ed1c5. You are right that these are established rather than born unlimited: native newaccount writes 0/0/0 and transfers the fixed gift, then setsyscode -> giftram sets -1 while funding the code RAM, with sysio.roa preserving it on later quota syncs. Conclusion kept, mechanism corrected.
| receives a **policy** — a grant of CPU, NET, and RAM weight — from a **node owner**, who holds a | ||
| fixed share of the network's capacity determined by their tier and issues slices of it through the | ||
| `sysio.roa` contract. Only registered node owners can issue policies, and only out of their own | ||
| tier budget — so an ordinary account cannot grant itself bandwidth, and no account can create |
There was a problem hiding this comment.
[P2] Scope policy-only capacity claims consistently
Privileged setalimits / setacctcpu / setacctnet can create positive resource weights outside ROA and node-owner budgets, while the fixed account gift and giftram allocate RAM without a policy on the recipient. The document acknowledges those paths later, so this opening invariant and the RAM ... arrives only through a policy / only on-chain distinction wording at lines 776–784 remain contradictory. Scope the claims to ordinary application provisioning beyond the account gift.
There was a problem hiding this comment.
Fixed in d2732ed1c5. The opening invariant now stops at "an ordinary account cannot grant itself bandwidth" and notes the privileged paths, and the RAM-market bullet is scoped to applications. The delegatebw bullet already enumerated giftram and the privileged setters, so the contradiction was between that and the two broader claims.
|
|
||
| Two anchors to calibrate against: | ||
|
|
||
| - **NET is small and predictable, but it is not just the action.** Each action is billed its own |
There was a problem hiding this comment.
[P3] Include context-free data in NET sizing
packed_transaction::get_action_billable_size also adds context_free_data[action_index].size() to the matching context-free action. Those bytes are neither the action's serialized size nor the shared header/signature/extension overhead described here. Add a clause for context-free actions so this sizing rule is complete.
There was a problem hiding this comment.
Fixed in d2732ed1c5. Added a clause: a context-free action is additionally billed the bytes of its matching context_free_data entry, per get_action_billable_size.
| **`extendpolicy`** — push `time_block` further out. It can only move forward, never back, and never | ||
| to a block already in the past. A policy's term can be lengthened but not shortened. | ||
|
|
||
| **`reducepolicy`** — take weight back. Only callable once `time_block` has passed. Each weight is |
There was a problem hiding this comment.
[P3] Say at or after time_block
reducepolicy checks current_block >= pol_row.time_block, so reduction is permitted in the block whose height equals time_block, not only after it has passed. Use at or after time_block here and in the repeated references at lines 79, 488, 801–802, and 832.
There was a problem hiding this comment.
Fixed in d2732ed1c5. current_block >= pol_row.time_block, so "at or after" -- corrected in all four places (comparison table, spam-control section, stacking section, reference table).
Seven corrections, kept tight -- net +11 lines. Native newaccount is not closed to everyone: apply_sysio_newaccount accepts it whenever the declared creator authorized the action and is privileged. Scoped to ordinary accounts rather than "nobody". System-account CPU/NET is established, not inherited. Production creates these accounts after sysio.system and ROA activation, so native newaccount writes 0/0/0 first and setsyscode -> giftram then sets -1 while funding the code RAM. The conclusion stands; the mechanism described was wrong. Failures are free only objectively. update_billed_cpu_time records CPU against the payer and non-exhaustion failures pass that map to subjective_bill_failure, with disable-subjective-payer-billing defaulting to false -- so failed spam does consume the contract payer's node-local headroom. NET sizing was incomplete: get_action_billable_size also adds the matching context_free_data entry for a context-free action. reducepolicy checks current_block >= time_block, so reduction is permitted in the block that equals it. "at or after" in all four places. Policy-only capacity claims scoped: privileged setalimits/setacctcpu/setacctnet and the fixed account gift create weights outside ROA, which the document already acknowledged later and contradicted in the opening invariant and the RAM-market bullet. The tier figures now carry a note that they describe current master and that #585 returns 0.1080 SYS to tier-2 and tier-3 budgets, merging after this PR and carrying the corresponding edit.
huangminghuang
left a comment
There was a problem hiding this comment.
The round-8 changes resolve the five previous inline findings, and current-head CI is green. I found two remaining documentation inconsistencies, noted inline.
Separately, the prior PR-description finding is still unresolved: the body still says the signer is not limit-checked, calls the throughput denominator ROA-only, presents the regular-action payer-pairing requirements as universal, uses the pre-#587 1068 / +288 RAM figures, categorically says own-limit failures accumulate, reports 869 rather than 885 lines, and says the PR is blocked on already-merged #590. Please refresh the full description to match the current document, including the inline-action and transaction-wide exemption corrections.
I am not approving this round because these reviewer- and user-facing statements remain inaccurate.
| A third-party contract is different. It is an ordinary account, and it is the payer for every call | ||
| into it that does not name one explicitly — which is every ordinary call — so it needs a real | ||
| allocation. That is exactly what a ROA policy provides. |
There was a problem hiding this comment.
[P2] Scope this payer rule to declared actions
This still says the callee is the payer for every ordinary call, but the corrected accounting section above establishes that only transaction-declared actions create CPU/NET billing entries; an inline callee runs inside the caller's declared action and uses the caller's payer. As written, this incorrectly tells composed-contract authors that every inline callee needs its own allocation. Please scope this paragraph to transaction-declared/top-level calls and update the same absolute at lines 715–716.
There was a problem hiding this comment.
Fixed in a2dee29c69, and you found the root cause rather than an instance. The payer rule was stated in eight places; last round I corrected the accounting section and left the other seven saying "every call".
Rather than patch this paragraph and lines 715-716 in isolation, the statement beside action::payer() is now the single authoritative one: keyed on payer(), one entry per action the transaction declares, and the consequence spelled out — a contract reached only inline has its work timed inside the declared action that triggered it and billed to that actions payer, so it needs no allocation of its own. The remaining sites are scoped to that rule instead of re-deriving it, and the duplicate inline explanation I had added to The three resources is cut back to the bare mechanism.
Both sites you named are scoped: "the payer for every action a transaction declares on it without naming one explicitly", and "the payer for any declared action that does not name one explicitly". Net one line, and a correction to the payer rule now has one place to land.
| and a retry that lands is billed once. Nor subjectively: `disable-subjective-payer-billing` defaults | ||
| true, so `subjective_bill_failure` skips the payer and the cost lands on the *signer*, if the | ||
| transaction reached authorization — see [Subjective billing](#subjective-billing-meters-the-signer). |
There was a problem hiding this comment.
[P2] Require successful authorization before signer CPU accrues
reached authorization is too broad: a transaction that fails check_authorization reaches that stage but fails before exec() creates any billed_cpu_us, leaving no authorizer CPU for subjective_bill_failure to accumulate. The signer cost described here begins only after authorization succeeds and action execution records CPU. Please say that, rather than implying failed authorization is billed.
There was a problem hiding this comment.
Fixed in a2dee29c69 — now "once authorization succeeds" rather than "reached".
You are right that "reached" was too broad: a transaction failing check_authorization reaches that stage but never enters exec(), so billed_cpu_us stays empty, authorizers_cpu and accounts_billing are empty when subjective_bill_failure is called, and auth_verified is false so the producer counter skips it too.
One refinement on the wording though. I did not use "and action execution records CPU", because that reads as though the action must succeed. It does not: exec() wraps execute_action in a catch that records the elapsed time into billed_cpu_us before rethrowing, so a failing action bills the signer just as a successful one does — which is the behaviour the failure limiter depends on. Authorization succeeding is the whole boundary, so the text stops there.
Both findings this round were stale restatements left behind by the previous round's correction: the rule appeared in eight places and only the one that was edited said "declared action". Rather than patch the two that were flagged, the statement beside action::payer() now carries the complete rule -- keyed on payer(), one entry per action the transaction declares, and the consequence that a contract reached only inline needs no allocation of its own. The other sites are scoped to it instead of re-deriving it, and the duplicate inline explanation added to The three resources last round is cut back to the mechanism. The signer accrues subjective CPU once authorization succeeds, not once it is reached. A transaction that fails check_authorization never runs exec(), so billed_cpu_us stays empty and both the accumulator and the failure counter skip it. Past that point even a throwing action bills, since the catch around execute_action records its time before rethrowing.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit resolves the two comments from the previous round, but the consolidated payer paragraph introduces two new inaccuracies and leaves related absolutes elsewhere; I have noted those inline, along with the remaining post-authorization billing boundary.
Separately, the PR-description blocker is still unaddressed: GitHub reports lastEditedAt: 2026-08-28T21:40:06Z, before the latest review rounds. It still says the signer is not limit-checked despite default-on subjective metering, calls the denominator ROA-only, uses the pre-#587 1068 / +288 RAM transcript, categorically describes own-limit failure accumulation, reports 869 rather than the current 886 lines, and says the PR is blocked on already-merged #590. Please refresh the complete description to match the current document and validation, including the inline-action, RAM, authorization-stage, and transaction-wide exemption corrections.
I am not approving this head while these user-facing statements remain inaccurate. Exact-head build CI is also still in progress.
| **A contract without a policy does not run.** Because the contract is the payer, an unprovisioned | ||
| one has nothing to pay with, and ordinary calls into it fail. Provisioning the contract — not the | ||
| user — is what makes an application usable. | ||
|
|
||
| There is one exception, covered in [Who pays](#who-pays-the-payer-model): an account can volunteer | ||
| to pay for itself. It is opt-in, it requires signatures, and it is not how ordinary traffic works. |
There was a problem hiding this comment.
[P2] Include the inline-callee path in the short version
The bold absolute says an unprovisioned contract does not run and then presents self-pay as the only exception, but lines 159–163 correctly recognize another path: a contract reached only inline executes inside the caller's declared action without its own CPU/NET billing entry. Please scope this short version to actions declared directly on the contract. The same declared-action qualification is still needed in the flowchart at line 155 and the claim at lines 260–261 that only a caller willing to pay can reach an unprovisioned contract.
There was a problem hiding this comment.
Fixed in 93d4cf56b4, swept rather than patched.
This is the third consecutive round where a correction to the billing rule left its restatements stale, so rather than scope the three sites you named I went through every absolute in the page in one pass. The short version now says the contract is the payer "for actions declared on it", and the flowchart node reads "Contract needs a policy for actions declared on it".
Lines 260-261 are deleted rather than scoped. The paragraph immediately above already states the same thing more precisely — that a provisioned caller or relayer can drive an otherwise unprovisioned contract given capacity and RAM coverage — so the summary was restating a rule that had just been given exactly.
I also added a check I should have been running since round 8: grep the page for the absolutes (does not run, every call, only a caller, no allocation, inert) and confirm each survivor is scoped or in a context where it is correct. The remaining hits are about the signer, node owners managing policies, and the error table, none of which take the declared-action qualification. Net two lines shorter.
| The objective billing map is keyed on `payer()` and nothing else, and it holds one entry per action | ||
| the transaction *declares*. An authorizing account that is not the payer never enters it, so |
There was a problem hiding this comment.
[P2] Aggregate billing entries by payer
accounts_billing is a map keyed by account, and init() updates accounts_billing[act.payer()]; multiple declared actions sharing a payer therefore accumulate into one entry. The declared-action boundary is correct, but “one entry per action” is not and conflicts with the preceding “keyed on payer().” Please say the map is populated only from transaction-declared actions, with their usage aggregated by payer.
There was a problem hiding this comment.
Fixed in 93d4cf56b4. You are right and the sentence was self-contradictory — I wrote "keyed on payer()" and "one entry per action" side by side, and only one of those can be true. accounts_billing[payer].cpu_usage_us += b.value is a map keyed by account, so two declared actions sharing a payer aggregate into a single entry.
It now reads that the map is populated only from the actions a transaction declares, is keyed on payer(), and that two declared actions sharing a payer aggregate into one entry. That was my error from last round, not a pre-existing one.
| consensus neither charges nor limits its CPU and NET. Nor does a contract reached only inline: its | ||
| work is timed inside the declared action that triggered it and billed to that action's payer, so it | ||
| needs no allocation of its own. A producer running |
There was a problem hiding this comment.
[P2] Preserve RAM requirements for inline callees
The inline callee has no separate CPU/NET bill, but “needs no allocation of its own” is too broad. RAM remains independent: update_db_usage(payer) feeds transaction_context::add_ram_usage, which validates that RAM payer regardless of the declared-action billing map. If an inline callee creates state billed to itself, it still needs sufficient RAM capacity. Please say it needs no separate CPU/NET allocation for the inline work and preserve the normal RAM-payer requirement.
There was a problem hiding this comment.
Fixed in 93d4cf56b4. Correct — I generalized a CPU/NET property to all three resources, which is wrong.
update_db_usage(payer) feeds transaction_context::add_ram_usage, which validates that RAM payer independently of the declared-action billing map, so an inline callee creating state billed to itself still needs the capacity. The sentence now says the inline callee needs no CPU or NET allocation for that work, followed by "RAM is separate — a callee that bills state to itself still needs the capacity."
| and a retry that lands is billed once. Nor subjectively: `disable-subjective-payer-billing` defaults | ||
| true, so `subjective_bill_failure` skips the payer and the cost lands on the *signer*, once | ||
| authorization succeeds — see [Subjective billing](#subjective-billing-meters-the-signer). |
There was a problem hiding this comment.
[P2] Tie signer billing to recorded action CPU
Authorization success sets auth_verified before exec(), but does not itself create any billed_cpu_us. For example, exec() calls checktime() before entering the execute_action try/catch; a post-authorization, pre-action limit failure leaves authorizers_cpu empty, so there is no signer CPU for subjective_bill_failure to accumulate. Once action execution starts, the catch correctly records CPU even when the action throws. Please describe that boundary rather than saying authorization success alone makes the cost land on the signer.
There was a problem hiding this comment.
Fixed in 93d4cf56b4 — and you were right last round and I was wrong to push back.
I argued that authorization succeeding was the whole boundary. It is not: exec() calls checktime() before entering the execute_action try/catch, so a post-authorization, pre-action deadline failure leaves billed_cpu_us empty and there is nothing for subjective_bill_failure to accumulate. My objection was only to phrasing that could read as requiring the action to succeed — which it does not, since the catch records elapsed time before rethrowing — but that does not rescue the claim I made.
The text now says the cost lands on the signer once an action begins executing.
Two of these were errors introduced into the canonical sentence last round. accounts_billing is a map keyed by account -- accounts_billing[payer] accumulates -- so "one entry per action" was wrong and contradicted "keyed on payer()" in the same sentence. It now says the map is populated only from declared actions and aggregated by payer. And "needs no allocation of its own" generalized a CPU/NET property to RAM: update_db_usage feeds add_ram_usage, which validates the RAM payer independently, so an inline callee billing state to itself still needs capacity. The signer accrues once an action begins executing, not once authorization succeeds. exec() calls checktime() before entering the execute_action try/catch, so a post-authorization deadline failure records no CPU at all. Past that point even a throwing action bills, since the catch records before rethrowing. The remaining restatements are swept in one pass rather than patched individually: the short version and the flowchart now scope to actions declared on the contract, and the paragraph asserting only a paying caller can reach an unprovisioned contract is deleted, since the paragraph above it already says that more precisely.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit resolves all four inline findings from the previous round, and exact-head CI is fully green. I found two remaining summary-level inconsistencies, noted inline.
Separately, the PR-description blocker is still unaddressed: GitHub reports lastEditedAt: 2026-08-28T21:40:06Z. The body still says the signer is not limit-checked despite default-on subjective metering, presents regular-action payer pairing as universal, calls the throughput denominator ROA-only, retains the pre-#587 1068 / +288 RAM transcript, categorically describes own-limit failure accumulation, reports 869 rather than the current 884 lines, and says #590 is still blocking. Please replace the accumulated historical description with a concise current summary of the complete PR, including declared-versus-inline billing, RAM's independent payer/capacity path, the action-execution boundary for subjective CPU, transaction-wide account exemptions, current validation, and the now-merged prerequisites.
I am not approving this head while these reviewer- and user-facing statements remain inaccurate.
| **A contract without a policy does not run.** Because the contract is the payer for actions declared | ||
| on it, an unprovisioned one has nothing to pay with and those calls fail. Provisioning the contract | ||
| — not the user — is what makes an application usable. | ||
|
|
||
| There is one exception, covered in [Who pays](#who-pays-the-payer-model): an account can volunteer | ||
| to pay for itself. It is opt-in, it requires signatures, and it is not how ordinary traffic works. |
There was a problem hiding this comment.
[P2] Scope the headline and exception too
The new rationale correctly scopes the failure to actions declared on the contract, but the bold headline still says an unprovisioned contract does not run and the next paragraph still presents self-pay as the one exception. A contract reached only inline is another execution path: its CPU/NET run under the caller's declared-action payer, subject to the separate RAM rule described later. Please make the headline itself about directly declared calls and scope “one exception” to payer selection for those calls.
There was a problem hiding this comment.
Fixed in a6e9b5e826. You are right, and the miss is worth naming: last round I swept for exactly this absolute and found this line, but scoped only the rationale beneath the headline and left the bold sentence itself unqualified — which is the part a reader meets first.
The headline now reads "A contract without a policy does not run when called directly", and the paragraph after it says "For those calls there is one exception to who pays", so self-pay is scoped to payer selection for directly declared actions rather than presented as the only other way a contract can execute. The inline path is described where the rule lives, in Who pays.
|
|
||
| | | EOS staking | REX | PowerUp | **Wire ROA** | | ||
| |---|---|---|---|---| | ||
| | Who is billed for a transaction | Signer | Signer | Signer | **Called contract, unless the action names an explicit payer** | |
There was a problem hiding this comment.
[P2] Describe the payer per declared action
This row asks who is billed for a transaction but answers with one singular “called contract.” transaction_context::init() processes every declared action and aggregates usage by each action's payer(), so a multi-action transaction can bill several contracts and/or explicit payers; inline callees add no separate entry. Please make this “Each declared action's contract, unless that action names an explicit payer” (or equivalent).
There was a problem hiding this comment.
Fixed in a6e9b5e826. The row asked a per-transaction question and answered with a single contract, which is wrong for any multi-action transaction.
It now reads "Each declared action's contract, unless that action names an explicit payer". That matches the aggregation: init() walks every declared action and accumulates into accounts_billing keyed by each action's payer(), so one transaction can bill several contracts, several explicit payers, or a mix — while inline callees add no entry of their own.
The headline still asserted that an unprovisioned contract does not run, and the paragraph after it framed self-pay as the only exception. Last round I scoped the rationale beneath the headline but not the headline itself, so the absolute survived where a reader meets it first. It now says "when called directly", and the exception is scoped to who pays for those calls rather than presented as the only other execution path. The comparison row answered a per-transaction question with one contract. init() processes every declared action and aggregates usage by each action's payer(), so a multi-action transaction can bill several contracts or explicit payers.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit resolves both comments from the previous round, and exact-head CI is fully green. I found two remaining summary-level contradictions, noted inline.
Separately, the PR description is still unchanged since 2026-08-28T21:40:06Z and materially disagrees with the current 885-line document. Among other things, it still says the signer is not limit-checked despite default-on subjective metering, presents the regular-action payer pairing as universal, calls the throughput denominator ROA-only, retains the pre-#587 1068 / +288 RAM transcript, categorically describes own-limit failure accumulation, reports 869 lines, and says the PR is blocked on already-merged #590. Please replace the accumulated historical body with a concise current summary of the complete PR, including declared-versus-inline billing, RAM's independent payer/capacity path, the action-execution boundary for signer subjective CPU, transaction-wide account exemptions, current validation, and the merged prerequisites.
I am not approving this head while these reviewer- and user-facing statements remain inaccurate.
| sit outside this: `setalimits` and the fixed account gift can set limits without a policy.) A node | ||
| owner may name itself as its own policy's recipient; registration does that for tier 1 only. | ||
|
|
||
| **A contract without a policy does not run when called directly.** Because the contract is the payer |
There was a problem hiding this comment.
[P2] Keep explicit-payer calls out of this headline
“when called directly” excludes the inline-callee path, but it still includes a directly declared action that opts into explicit self-pay. Lines 256–260 correctly explain that a provisioned caller or relayer can drive an unprovisioned contract, so this bold headline contradicts the documented behavior even though the next paragraph introduces the exception. Please qualify the headline with “under default billing” (or otherwise make both conditions explicit).
There was a problem hiding this comment.
Agreed. Headline now reads "Under default billing, a contract without a policy does not run when called directly," and the exception paragraph below it says explicitly that self-pay takes the contract's own allocation out of the picture — so the short version points at the lines 256–260 treatment rather than contradicting it.
| **The default: the called contract pays.** An ordinary transaction names no payer at all, so the | ||
| account billed for an action is the contract that action invokes. Nothing in the transaction has | ||
| to say so and no permission has to be added — this is what happens when you do nothing special, |
There was a problem hiding this comment.
[P2] Scope default payer selection to declared actions
This says the invoked contract is billed for “an action,” which also reads as covering inline actions. transaction_context::init() builds accounts_billing only from the transaction's declared context-free and regular actions; an inline callee adds no billing entry, and its CPU stays with the originating declared action's payer, as lines 160–164 correctly explain. Please say “for each action the transaction declares” here and carry the same qualification into the short-version sentence at lines 16–18.
There was a problem hiding this comment.
Agreed — accounts_billing is populated only from trx.context_free_actions and trx.actions, so an inline callee never gets an entry. Changed to "the account billed for each action the transaction declares," and carried the same qualification into the short version: "billed to the contract each declared action calls."
The short-version headline read as an absolute, but an action naming an explicit payer bills that account and never consults the contract's own allocation. The payer-model default likewise said "an action" where accounts_billing is built only from trx.context_free_actions and trx.actions.
huangminghuang
left a comment
There was a problem hiding this comment.
The two inline findings from the previous round are fixed correctly. I found two remaining documentation-correctness issues, noted inline. Exact-head CI has no failures so far, although the five build/test variants are still running.
Separately, the PR description remains unedited since 2026-08-28T21:40:06Z and still materially contradicts the current 887-line document: it says the signer is not limit-checked despite default-on subjective metering, presents the regular-action payer pairing as universal, calls the throughput denominator ROA-only, retains the pre-#587 1068 / +288 RAM transcript, says rows are born -1, reports 869 lines, and says the PR is blocked on already-merged #590. Please replace the accumulated historical body with a concise current summary of the complete PR and its current validation, as required after review follow-ups.
I am not approving this head while the document and its PR description retain these inaccuracies.
| [Who pays](#who-pays-the-payer-model): an account can volunteer to pay for itself, which takes the | ||
| contract's own allocation out of the picture. It is opt-in, it requires signatures, and it is not |
There was a problem hiding this comment.
[P2] Keep RAM out of this self-pay claim
This new sentence says self-pay takes the contract's own “allocation” out of the picture, but an explicit payer redirects only the declared action's CPU/NET billing entry. RAM is validated independently against the account receiving the RAM delta; as lines 167 and 258–262 correctly explain, a contract that bills state to itself still needs RAM capacity. Please scope this to the contract's own CPU and NET allocation.
There was a problem hiding this comment.
Right, and that was my wording from the last round. Scoped to "takes the contract's own CPU and NET out of the picture."
| > **Compared with Antelope.** There, a contract names a RAM payer in the action body and the | ||
| > requirement is only that the named account authorized the action at all — CPU and NET follow a | ||
| > separate path entirely, funded by the signer's stake or rental. On Wire the two are welded | ||
| > together: naming a user as RAM payer requires the `sysio.payer` marker, and because that marker | ||
| > must sit at index 0 it makes the same user the CPU and NET payer for the action. There is no way | ||
| > to charge a user for storage while the contract absorbs their bandwidth. |
There was a problem hiding this comment.
[P2] Preserve the inline RAM-only payer path
This “no way” claim is true only when the sysio.payer marker is on a transaction-declared action. transaction_context::init() builds accounts_billing only from declared actions, while apply_context::validate_account_ram_deltas() checks the authorizations on the currently executing action, including an inline action. An inline action can therefore carry a user payer marker plus a satisfiable real user permission to authorize RAM charged to that user, while its CPU/NET remains inside the originating declared action and is billed to that action's payer. Please qualify this comparison to declared actions, explain the inline exception, and apply the same scope to the generic explicit-payer flowchart branch around line 159.
There was a problem hiding this comment.
Confirmed and corrected. execute_inline routes inline actions through the same check_authorization overload, so the index-0 and paired-permission rules hold there, while billed_cpu_us.size() == trx.total_actions() keeps the inline action out of the billing map — and validate_account_ram_deltas scans the executing action's authorizations. So the split does survive inline.
The comparison now says the marker makes the user the CPU/NET payer on a declared action, and names the inline path as where the split remains. Flowchart node H and the paragraph under it carry the same scope; the full treatment lives with the billing-map paragraph, with the callout pointing there. The one precondition I added: the inline {user, <real perm>} has to be satisfiable by {contract, sysio.code}, so it needs the user's code-permission grant rather than being unilateral.
…RAM path An explicit payer redirects only the declared action's CPU/NET billing entry; RAM is validated separately against the executing action's authorizations. That also makes the Antelope comparison's "no way to charge a user for storage while the contract absorbs their bandwidth" wrong on the inline path -- check_authorization runs for inline actions via execute_inline, but transaction_context::init never sees them.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit correctly fixes both inline findings from the previous round. Exact-head CI is fully green, the diff is clean, and this head merges cleanly into current master. I found two remaining inline-action inconsistencies, noted inline.
Separately, the PR description remains unedited since 2026-08-28T21:40:06Z and materially contradicts the current 892-line document. It still says the signer is not limit-checked despite default-on subjective metering, presents regular-action payer pairing as universal, calls the throughput denominator ROA-only, retains the pre-#587 1068 / +288 RAM transcript, reports 869 lines, and says the PR is blocked on already-merged #590. Please replace the accumulated historical body with a concise current summary of the complete PR and its current validation, as required after review follow-ups.
I am not approving this head while these document and PR-description inaccuracies remain.
| B -->|"Yes — opt-in"| D{"Same actor also present<br/>with a real permission,<br/>and signed for?"} | ||
| D -->|"No"| E["Rejected:<br/>unsatisfied authorization"] | ||
| D -->|"Yes"| F["Payer = that actor"] | ||
| C --> G["Contract needs a policy for<br/>actions declared on it.<br/>Signer not billed."] | ||
| F --> H["Declared action: actor billed CPU and NET,<br/>contract not. Either way the marker<br/>authorizes RAM billed to the actor."] |
There was a problem hiding this comment.
[P2] Show the inline authorization route
The updated destination now covers inline RAM billing (“Either way”), but every path to it still passes through “and signed for?”. A valid inline action need not carry the user's transaction signature: execute_inline() supplies {receiver, sysio.code}, which can satisfy the user's paired real permission through their standing code grant. As drawn, the diagram rejects the exact inline RAM-only path this change is documenting. Please split declared versus inline authorization, or phrase the check as “paired real permission satisfied (by transaction signatures or receiver@sysio.code).”
There was a problem hiding this comment.
Right — the gate as drawn rejected the path the previous commit added. Node D now reads "satisfied by signatures or, inline, by receiver@sysio.code", which is what check_authorization actually takes: provided_keys on the declared path, provided_permissions from execute_inline on the inline one.
| > `Requested payer ... Missing sysio.payer`. And because `sysio.payer` must sit at index 0, adding | ||
| > it also makes that user the action's CPU and NET payer. | ||
| > | ||
| > So billing RAM to a user is an explicit opt-in by the user, not a choice the contract makes | ||
| > alone, and it opts them into paying for bandwidth at the same time. Contract authors who want the |
There was a problem hiding this comment.
[P2] Scope bandwidth opt-in to declared actions
These sentences still say adding sysio.payer makes the user pay CPU/NET and that RAM opt-in necessarily opts them into bandwidth. That is true only for a transaction-declared action: the objective billing map excludes inline actions, so the inline marker can authorize user-paid RAM while CPU/NET stay with the originating declared action's payer, exactly as lines 170–173 and 655–658 now explain. Please qualify this paragraph to declared actions and align the related generic wording at lines 179 and 723–725 so RAM remains clearly independent.
There was a problem hiding this comment.
Fixed in all three. The callout now says the marker makes the user the CPU/NET payer on a declared action and authorizes RAM alone on an inline one; the developer section says "billed CPU and NET only if they opt in with sysio.payer on a declared action"; and the payer table's notes carry the same split rather than the unscoped "needs her own allocation" / "covers the whole action's cost."
The flowchart's "and signed for?" gate rejected the inline RAM-only path
the previous commit documented -- execute_inline supplies
{receiver, sysio.code}, which satisfies the paired permission without a
transaction signature. The RAM callout and the developer section still
said the RAM opt-in necessarily opts a user into bandwidth, and the payer
table's notes were unscoped.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit correctly fixes both inline findings from the previous round, and the PR description has now been refreshed. I found four remaining documentation boundary errors, noted inline. Exact-head CI has no failures so far, although five build/test variants are still running; the diff is clean and GitHub reports the head mergeable.
The refreshed PR description also needs another accuracy pass. It still says the ordinary signer is not limit-checked without objective/consensus scope; says an inline callee's policy is never consulted despite self-billed RAM; says a declared payer marker makes the user the RAM payer rather than authorizing RAM the contract elects to bill there; generalizes subjective signer billing to failed transactions without the authorization/action/exhaustion boundaries; and says system rows are born -1 even though newaccount writes 0/0/0 before setsyscode → giftram establishes unlimited CPU/NET. Please align the reviewer-facing summary with the corrected document and implementation.
I am not approving this head while these user-facing statements remain inaccurate.
| You can only volunteer yourself, or someone who co-signs. An account that names itself payer needs | ||
| its own allocation and fails without one. |
There was a problem hiding this comment.
[P2] Preserve delegated inline authorization
The regular-action requirements above are scoped correctly, but this conclusion drops that scope. An inline action can bill a user's RAM without that user co-signing the current transaction when a standing permission delegates to receiver@sysio.code; because inline actions do not enter the objective CPU/NET billing map, naming that user also does not require their own CPU/NET allocation. Please scope both sentences to transaction-declared regular actions and state the delegated inline exception.
There was a problem hiding this comment.
Scoped. Reads "On a declared regular action you can only volunteer yourself, or someone who co-signs, and that account needs its own CPU and NET allocation," pointing at the inline paragraph below for both differences rather than restating them here.
| A["Action arrives"] --> B{"authorization[0].permission<br/>== sysio.payer ?"} | ||
| B -->|"No — ordinary traffic"| C["Payer = the contract being called"] | ||
| B -->|"Yes — opt-in"| D{"Same actor also present with a<br/>real permission, satisfied by signatures<br/>or, inline, by receiver@sysio.code ?"} | ||
| D -->|"No"| E["Rejected:<br/>unsatisfied authorization"] | ||
| D -->|"Yes"| F["Payer = that actor"] | ||
| C --> G["Contract needs a policy for<br/>actions declared on it.<br/>Signer not billed."] | ||
| F --> H["Declared action: actor billed CPU and NET,<br/>contract not. Either way the marker<br/>authorizes RAM billed to the actor."] |
There was a problem hiding this comment.
[P2] Account for the context-free payer form
This generic “Action arrives” flow still routes every payer marker through a same-action real-permission check. A valid context-free action may instead carry exactly one {alice, sysio.payer} marker when Alice is already an explicit payer on a regular action in the transaction; adding the paired real permission to the context-free action is rejected. Please scope this chart to non-context-free actions or add the context-free inheritance branch.
There was a problem hiding this comment.
Confirmed — verify_auth in validate_referenced_accounts accepts a context-free action only at authorization.size() == 0, or == 1 carrying the marker whose actor is an explicit payer on some regular action, so adding the paired permission makes it size 2 and it fails.
The chart's entry node now says "Regular action arrives," and the requirements list above it gained the context-free form, which the page had never stated.
| **One porting change is not optional, though.** A great many Antelope contracts name an | ||
| authorizing user as the RAM payer for rows they create — the `{user, "active"_n}` idiom, with | ||
| `user` passed to `emplace`. Wire rejects that transaction whatever policy the contract holds: | ||
| `validate_account_ram_deltas` requires a payer other than the receiver to carry the `sysio.payer` | ||
| permission on the action, and `active` does not satisfy it: | ||
|
|
||
| ``` | ||
| Requested payer alice did not authorize payment. Missing sysio.payer. | ||
| ``` | ||
|
|
||
| Two ways through, and the choice is a product decision: | ||
|
|
||
| - **Bill the rows to the contract** (`emplace(get_self(), ...)`). The contract's policy covers the | ||
| storage, users stay free, and the caller's transaction is unchanged. This is the gasless path, | ||
| and what most ports want. | ||
| - **Keep billing the user**, which requires the client to add `sysio.payer` at index 0 — making | ||
| the user the CPU and NET payer too, so they need their own allocation. |
There was a problem hiding this comment.
[P2] Handle RAM writes from notification handlers
These two porting options do not cover an unprivileged notification handler. validate_account_ram_deltas rejects any positive delta billed to payer != receiver while receiver != act.account before it checks the payer marker, so even a user who authorized the original action with sysio.payer cannot fund RAM written by the notified contract. Scope the user-paid option to direct/inline execution and explain that a notification handler must bill its own receiver account or refactor the write into a direct/inline action.
There was a problem hiding this comment.
Right — the not_in_notify_context assert fires for any positive delta to an account other than the receiver, before the marker scan. Added to the keep-billing-the-user option: it is closed to a notification handler, which bills its own account or moves the write into a direct or inline action. The bill-to-contract option is unaffected, since get_self() is the receiver there.
| For a **user** account, consensus never consults those zeros. It signs, the contract pays, the | ||
| transaction succeeds. |
There was a problem hiding this comment.
[P2] Scope the user's zero limits to contract-paid calls
Consensus does consult a user's zero CPU/NET limits when that user opts into explicit self-pay; the example at lines 778–788 correctly shows the zero-limit user failing. This statement is true only for ordinary/default contract-paid calls, so please add that qualification here and in the corresponding PR-description claim.
There was a problem hiding this comment.
Correct, and the self-pay example further down contradicted it. Now "For a user account making an ordinary contract-paid call... Opt into self-pay and they are consulted like any other payer's." Same fix applied to the PR description.
Four scope gaps, all confirmed against the validators. validate_referenced_accounts accepts a context-free action carrying the marker alone -- and rejects one that adds the paired permission -- provided the actor is already an explicit payer on a regular action; the page had only the regular-action form. validate_account_ram_deltas asserts not_in_notify_context before it scans for the marker, so the keep-billing-the-user port is unavailable to a notification handler. The co-sign conclusion and the user's never-consulted zeros were both stated without their declared-action scope.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit fixes the four findings from the previous round. I found two remaining execution-mode boundary errors, noted inline.
The refreshed PR description also still needs an accuracy pass:
- “the signer is neither charged nor limit-checked” needs objective/consensus scope because default-on subjective billing meters and checks the signer;
- for inline work, the callee's CPU/NET allocation is not consulted, but its RAM capacity still is when it bills state to itself;
- on a declared action,
sysio.payerselects the CPU/NET payer and authorizes RAM billed to that actor; it does not itself make that actor the RAM payer; - failed transactions reach signer subjective billing only once action execution records CPU, and exhaustion/duplicate failures are excluded;
- system accounts begin with 0/0/0 resource limits before
setsyscode→giftramestablishes unlimited CPU/NET.
The mechanical summary is stale too: the document is now 901 lines, not 893, and “Nine rounds” is no longer current. Please align the full reviewer-facing description with the implementation and current head.
Exact-head CI currently has no failures, with five build/test jobs still running. The diff is clean and GitHub reports it mergeable. I am not approving this head while these documentation claims remain inaccurate.
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| A["Regular action arrives"] --> B{"authorization[0].permission<br/>== sysio.payer ?"} |
There was a problem hiding this comment.
[P2] Separate the declared, inline, and notification paths
Node D explicitly admits inline actions, but the chart then funnels them through declared-action outcomes. Inline actions never enter accounts_billing, so their CPU/NET remain with the originating declared action's payer; a privileged inline sender also skips this authorization gate, while an unprivileged notification execution cannot use the marker to bill RAM away from the receiver. Please split these execution modes (and scope the signature requirements above to transaction-declared regular actions), or constrain the chart to one mode. As drawn, Payer = …, Rejected, and Either way are each incorrect for at least one path the chart says it covers.
There was a problem hiding this comment.
Agreed — widening node D to admit inline execution is what made the outcomes wrong, since the chart's terminals were still the declared-action ones.
Rather than grow it into three modes, it is one mode again: entry node reads "Transaction-declared regular action," node D is back to signatures, and node H to the actor being billed CPU, NET and RAM with the contract not billed. A caption under it names the other cases and points down — the context-free form is the paragraph above, inline CPU/NET and inline RAM are the two paragraphs below, and the unprivileged-notify rule is in the porting section. Every one of those is somewhere the reader lands anyway; the chart was the wrong place to compress them.
| and what most ports want. | ||
| - **Keep billing the user**, which requires the client to add `sysio.payer` at index 0 — making | ||
| the user the CPU and NET payer too, so they need their own allocation. This one is closed to a | ||
| notification handler: `validate_account_ram_deltas` rejects any positive delta billed away from |
There was a problem hiding this comment.
[P2] Retain the privilege qualifier
validate_account_ram_deltas rejects this only when !privileged (apply_context.cpp:202). A privileged notified receiver skips the notify-context assertion and can proceed through the later marker/system bypass checks, so “closed to a notification handler” and “rejects any positive delta” are too broad. Please say “an unprivileged notification handler,” matching the actual guard.
There was a problem hiding this comment.
Correct — the assert is inside !privileged, so a privileged notified receiver falls through to the marker and system-account bypasses. Now reads "closed to an unprivileged notification handler" and "rejects a positive delta" rather than "any."
Widening node D to admit inline actions made the chart wrong on the paths it then claimed to cover: inline actions never enter accounts_billing, a privileged inline sender skips execute_inline's authorization gate entirely, and an unprivileged notify execution cannot use the marker to bill RAM away from the receiver. The chart is one mode again, with a caption pointing at the prose that carries the others. The notify-context assert is also inside !privileged, so the porting bullet says so.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit correctly fixes both findings from the previous round. I found one remaining RAM-boundary error in the revised chart, noted inline.
The PR description is still unedited since 2026-09-07T15:27:51Z, before this follow-up commit, so the five prior description findings remain: it leaves “neither charged nor limit-checked” unscoped from subjective billing; says an inline callee's policy is never consulted despite self-billed RAM; says the marker makes the actor the RAM payer; generalizes signer subjective billing to all failures; and says system rows are born at -1 rather than 0/0/0 before setsyscode → giftram. It also still reports 893 lines and “Nine rounds,” while the current document has 904 lines. Please update the complete description to match this head.
Exact-head CI currently has no failures, with five build/test variants still running. The diff is clean and the head merges cleanly into current master. I am not approving while the document and reviewer-facing description retain these inaccuracies.
| D -->|"No"| E["Rejected:<br/>unsatisfied authorization"] | ||
| D -->|"Yes"| F["Payer = that actor"] | ||
| C --> G["Contract needs a policy for<br/>actions declared on it.<br/>Signer not billed."] | ||
| F --> H["Actor is billed CPU, NET,<br/>and RAM billed to it.<br/>Contract not billed."] |
There was a problem hiding this comment.
[P2] Keep RAM independent in this terminal
sysio.payer selects the actor as the declared action's CPU/NET payer; it does not redirect all RAM. auth_ram_tests (unittests/ram_tests.cpp:222-293) verifies the exact counterexample: with Alice as explicit payer, Alice's CPU/NET rise while the contract's self-billed RAM rises and Alice's RAM does not. Therefore “Contract not billed” is false here after RAM has been included. Please say that the actor is billed CPU/NET, the marker authorizes RAM billed to the actor, and the contract may still bill RAM to itself.
There was a problem hiding this comment.
Confirmed, and it was introduced when RAM was folded into that terminal last round. auth_ram_tests pins it — the explicit-payer block asserts noauthtable_ram_usage2 < noauthtable_ram_usage3 alongside alice_ram_usage2 == alice_ram_usage3, since no_auth_table inserts with emplace(get_self(), ...).
Node H now carries all three facts: actor billed CPU and NET with the contract not, the marker authorizing RAM billed to the actor, and the contract still able to bill RAM to itself.
Minor: the test is at unittests/ram_tests.cpp:82-158; 222-293 is updateauth_ram_billing and the first setcode case.
Folding RAM into node H last round made "Contract not billed" false: the
marker moves CPU/NET to the actor and authorizes RAM billed to it, but the
contract still bills whatever it emplaces to get_self(). auth_ram_tests
pins exactly that -- with {alice, sysio.payer} the contract's RAM rises
while alice's does not.
huangminghuang
left a comment
There was a problem hiding this comment.
The latest commit correctly fixes the remaining RAM-boundary finding. I found no medium-or-higher issue in the current 904-line document itself.
I still cannot approve because the PR description has not been edited since 2026-09-07T15:27:51Z, before the last two follow-up commits, and remains materially inconsistent with the implementation and document:
- “the signer is neither charged nor limit-checked” lacks objective/consensus scope; default-on subjective billing checks the signer;
- “the callee's policy is never consulted” is too broad because self-billed RAM still needs callee capacity;
- a declared
sysio.payermarker selects CPU/NET billing and authorizes RAM billed to the actor; it does not make that actor the RAM payer; - not every failed transaction reaches signer subjective billing: action CPU must be recorded, and exhaustion/duplicate failures are excluded;
- system resource rows start at 0/0/0, with
setsyscode→giftramlater establishing unlimited CPU/NET.
The description also still says “Nine rounds” and 893 lines. Please refresh the complete PR description for this head as required after review follow-ups.
Exact-head CI currently has no failures, with five build/test jobs still running. The diff is clean and GitHub reports the head mergeable.
huangminghuang
left a comment
There was a problem hiding this comment.
The current 904-line document has no remaining medium-or-higher issue, and the refreshed PR description fixes most of the previous findings. Two description claims still need correction before approval:
- Under “What an unprovisioned account or contract looks like,” “Those zeros are never consulted for a user making an ordinary contract-paid call” needs objective/consensus scope. The default subjective precheck calls
get_cpu_limit(user)and incorporates that zero CPU limit into the user's available subjective allowance. - Under “Billing is per action…,” “the callee's policy is never consulted for inline work” is true only for CPU/NET. An inline callee that bills RAM to itself still needs RAM capacity;
finalize()callsverify_account_ram_usagefor that account. Please say the callee's CPU/NET allocation is not consulted.
Exact-head CI has no failures; ASan and the preliminary checks are green, with four build/test variants still running. The diff is clean and GitHub reports the head mergeable. I am not approving while these reviewer-facing statements remain inaccurate.
huangminghuang
left a comment
There was a problem hiding this comment.
Approved per the latest review. The documentation changes are ready to merge.
Summary
Adds
docs/roa-overview.md, a user-facing page on Resource Owner Allocation. Written for developers arriving from EOS/Antelope, and intended to be handed out directly — the immediate use is answering community questions about what a transaction actually costs on Wire.Documentation only; no code changes.
What it covers
action::payer()returnsauthorization[0].actorwhen that entry carries the reservedsysio.payerpermission, and the called contract otherwise. Ordinary transactions name no payer, so the contract is billed and the signer is neither charged nor limit-checked objectively — subjective billing, on by default, does meter and check the signer node-locally. Explicit self-pay is covered as the exception, including the requirement that the payer sit at index 0, carry a real permission on the same action, and have that permission satisfied — by transaction signatures on a declared action, or byreceiver@sysio.codeon an inline one. A context-free action takes the opposite form: the marker alone, valid only when that actor is already an explicit payer on one of the transaction's regular actions.sysio.payer, so a provisioned caller or relayer can drive a contract that has no policy of its own.bytes_per_unitand the RAM-reclaim interaction between issuers.total_cpu_weight/total_net_weight— every positive weight, not structurally ROA's.nodeop's subjective billing metering the signer.Verification
The behavioural claims were checked against the implementation with throwaway tests run against a full-system tester, then removed. The transcript blocks in the doc reproduce that output:
account payloadless net usage is too high: 132 > 00 → 2001µs, caller's CPU used stays0account alice net usage is too high: 148 > 0944 → 944even for a newly created balance row;sysio+144sysio_roa_tests/reduce_one_issuer_isolates_otherpassesReview
Every finding was verified against the implementation before the text changed, and the commit log carries them individually. The corrections that changed the shape of the page:
exec()snapshots the original action count before running anything and appends onebilled_cpu_usentry per original action, withexecute_actioncovering the whole recursive tree inside that timing. So a caller's payer covers a callee contract's CPU and the callee's policy is never consulted for inline work — which changes which account an author of composed contracts must provision.sysio.payermarker is not one thing. On a declared action it selects the user as the CPU and NET payer and authorizes RAM billed to that actor — it does not make them the RAM payer, and the contract still bills whatever it emplaces toget_self(), whichauth_ram_testspins directly. On an inline action it authorizes RAM alone —transaction_context::initnever sees the action, whilevalidate_account_ram_deltasreads the authorizations of whichever action is executing — and its paired permission is satisfied through the user's standingsysio.codegrant rather than a signature. The page had repeatedly stated the declared-action behaviour as universal, including a flat "there is no way to charge a user for storage while the contract absorbs their bandwidth."setcodebills code at ×10 (setcode_ram_bytes_multiplier). The deployment example is ~614,400 bytes / 0.5908 SYS, and contract-plus-ABI ~0.6 SYS rather than "under one tenth".sysio.prefix —privileged_kv_payer_bypasspins thatprivtest, privileged but unprefixed, still fails — plus a third bypass for any authorization whose actor issysio.{user, active}is rejected whatever policy it holds; where it bills RAM is a separate, mandatory decision.add_transaction_usageis reached only fromfinalize(). They reach the signer subjectively only once action execution has recorded CPU, and duplicates (tx_duplicate) and block-level exhaustion are excluded outright. A landed retry is billed once, and a successful transaction's subjective bill is released when it appears in a block.pending_cpu_usage/pending_net_usage.get_action_billable_sizeapportions the transaction overhead across actions, so signatures cost NET and batching amortizes it.giftram, and the privileged setters are separate routes, none open to applications. Nor does asysio.prefix confer unlimited limits: system accounts start at 0/0/0 and it issetsyscode→giftramthat later establishes-1, whileactivateroadeliberately leavessysio.acctat an explicit 0/0.reducepolicycaps RAM reclaim at the owner's unused quota, floored tobytes_per_unit, so consumed grant pins residual weight in the issuer's budget.ram_unusedis account-wide, so co-issuers draw on one shared pool. It is callable at or aftertime_block, not strictly after.addpolicyhas norequire_auth(owner), and registration writesissuer == owner.delegatebwand PowerUp both separate payer from receiver, PowerUp is not stateless, and its term ispowerup_daysrather than inherently daily.Length held roughly flat while the accuracy work happened: 904 lines now, against 939 at the end of round 2. Duplicate statements of the same claim were deleted rather than qualified in each place, so each rule has one authoritative treatment that the other sections link to.
Prerequisites (all merged)
Every implementation change this page depends on is on
master, and the branch has been merged up to it.check_authorizationruns. Naming someone on a transaction you cannot sign no longer throttles them.4119212403) — an implementation mismatch this review surfaced:get_required_keyshard-coded<payer>@activewhile consensus pairs thesysio.payermarker with any real permission, so self-pay underowneror a custom permission was accepted by the chain but unsignable through/v1/chain/get_required_keys.byusernameindex, halving a sponsorship row. Recomputed from the code as 280 → 144 bytes; Roa: drop the byusername secondary index on sponsors #584's own comment and body say~296 → ~152, 8 bytes high on both sides, sincebillable_size_v<kv_object>is 112 rather than an assumed 120. The halving is real; only the constants are off. This page uses the code-derived 144.28d130adcc) — the tier-1-only personal policy, reflected in the tier figures and the provisioning table.