Skip to content

Docs: ROA resource model overview - #583

Open
heifner wants to merge 27 commits into
masterfrom
docs/roa-overview
Open

Docs: ROA resource model overview#583
heifner wants to merge 27 commits into
masterfrom
docs/roa-overview

Conversation

@heifner

@heifner heifner commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

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

  • The payer model. action::payer() returns authorization[0].actor when that entry carries the reserved sysio.payer permission, 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 by receiver@sysio.code on 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.
  • Declared versus inline. The objective billing map is built only from the actions a transaction declares, so a contract reached only inline needs no CPU or NET allocation of its own — though it still needs RAM capacity for state it bills to itself. RAM is checked separately, against the authorizations on the action executing, which is what lets an inline action bill a user's RAM while its CPU and NET stay with the declared action's payer.
  • What an unprovisioned account or contract looks like. New accounts start at 0 CPU / 0 NET with 1,144 bytes of RAM. Those zeros are never consulted for a user making an ordinary contract-paid call, and are consulted like any other payer's once that user opts into self-pay. For a contract they are fatal under default billing — the contract is the payer, so an ordinary call fails rather than falling through to any other billing path. They are not consulted at all when an action names an explicit sysio.payer, so a provisioned caller or relayer can drive a contract that has no policy of its own.
  • Policies. Fields and semantics, who may issue (any registered node owner; there is no tier gate), tier budgets and caps, the four policy actions, and multi-issuer stacking, including the frozen bytes_per_unit and the RAM-reclaim interaction between issuers.
  • Throughput. The proportional-share formula, and specifically that its denominator is total_cpu_weight / total_net_weight — every positive weight, not structurally ROA's.
  • Spam control. Two layers: the payer account's own CPU/NET share as an always-on consensus cap, and nodeop's subjective billing metering the signer.
  • Scenarios for a token transfer, contract deployment sizing, trading, CPU exhaustion, onboarding, and self-pay.
  • Comparison to EOS staking, REX, and PowerUp.

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:

  • Unprovisioned contract called by a funded user → account payloadless net usage is too high: 132 > 0
  • Funding only the contract → same call succeeds; contract CPU 0 → 2001µs, caller's CPU used stays 0
  • Explicit self-pay with no allocation → account alice net usage is too high: 148 > 0
  • Token transfer RAM → sender and receiver both 944 → 944 even for a newly created balance row; sysio +144
  • Multi-issuer stacking → existing sysio_roa_tests/reduce_one_issuer_isolates_other passes

Review

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:

  • Billing is per action the transaction declares, not per action executed. exec() snapshots the original action count before running anything and appends one billed_cpu_us entry per original action, with execute_action covering 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.
  • The sysio.payer marker 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 to get_self(), which auth_ram_tests pins directly. On an inline action it authorizes RAM alone — transaction_context::init never sees the action, while validate_account_ram_deltas reads the authorizations of whichever action is executing — and its paired permission is satisfied through the user's standing sysio.code grant 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."
  • setcode bills 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".
  • The RAM-payer guarantee is scoped to unprivileged contracts. The bypass needs privilege and a sysio. prefix — privileged_kv_payer_bypass pins that privtest, privileged but unprefixed, still fails — plus a third bypass for any authorization whose actor is sysio.
  • Porting is not policy-only. An Antelope contract billing rows to {user, active} is rejected whatever policy it holds; where it bills RAM is a separate, mandatory decision.
  • Failed transactions do not bill objective CPU/NETadd_transaction_usage is reached only from finalize(). 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.
  • A contract's quota does not make the blast radius local — a successful transaction also drains block-wide pending_cpu_usage / pending_net_usage.
  • NET is not just the serialized actionget_action_billable_size apportions the transaction overhead across actions, so signatures cost NET and batching amortizes it.
  • Capacity is not policy-only — the 1,144-byte gift, giftram, and the privileged setters are separate routes, none open to applications. Nor does a sysio. prefix confer unlimited limits: system accounts start at 0/0/0 and it is setsyscodegiftram that later establishes -1, while activateroa deliberately leaves sysio.acct at an explicit 0/0.
  • reducepolicy caps RAM reclaim at the owner's unused quota, floored to bytes_per_unit, so consumed grant pins residual weight in the issuer's budget. ram_unused is account-wide, so co-issuers draw on one shared pool. It is callable at or after time_block, not strictly after.
  • Node owners can self-issueaddpolicy has no require_auth(owner), and registration writes issuer == owner.
  • Antelope comparison correcteddelegatebw and PowerUp both separate payer from receiver, PowerUp is not stateless, and its term is powerup_days rather 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.

  • #591 — subjective billing on by default, contract payer excluded. Raised from this review: the shipped defaults disabled it outright, which under contract-pays leaves the signer entirely unmetered. It also landed a fix for a defect found in its own review — the producer's failure counter attributed failures using the declared authorization list, which is attacker-chosen until check_authorization runs. Naming someone on a transaction you cannot sign no longer throttles them.
  • #590 (4119212403) — an implementation mismatch this review surfaced: get_required_keys hard-coded <payer>@active while consensus pairs the sysio.payer marker with any real permission, so self-pay under owner or a custom permission was accepted by the chain but unsignable through /v1/chain/get_required_keys.
  • #584 — dropped the byusername index, 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, since billable_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.
  • #585 (28d130adcc) — the tier-1-only personal policy, reflected in the tier figures and the provisioning table.
  • #587 — RAM billing corrections for permissions, keys and account init. The measured transcripts on this page were re-measured against it.

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.
@heifner
heifner requested a review from a team August 22, 2026 13:35
@heifner heifner added the documentation Improvements or additions to documentation label Aug 22, 2026
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.
@heifner
heifner requested a review from huangminghuang August 24, 2026 15:42
Comment thread docs/roa-overview.md Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md
Comment thread docs/roa-overview.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
| 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** |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@heifner
heifner requested a review from huangminghuang August 24, 2026 16:45
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
huangminghuang previously approved these changes Aug 24, 2026

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline documentation-correctness findings from a review of the current PR head.

Comment thread docs/roa-overview.md Outdated

| Item | Bytes | `ram_weight` |
|---|---|---|
| A 60 KB contract WASM | ~61,440 | ~0.0591 SYS |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • setabi is billed 1xnew_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_object row 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_object is shared by refcount, but the deployer still pays the full ×10), and a redeploy subtracts old_size * 10 first, so an upgrade costs only the difference. Both are in the callout.

Comment thread docs/roa-overview.md Outdated
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`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
### 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 additivelyavailable = 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.

Comment thread docs/roa-overview.md Outdated
- **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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated

### Why the contract can afford it

System accounts — anything whose name prefix is `sysio` — carry unlimited resource limits, and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
| 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
**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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@heifner
heifner requested a review from huangminghuang August 26, 2026 13:34

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline findings from the re-review of the current PR head.

Comment thread docs/roa-overview.md
- 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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."]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated

| Pool | Size | Funds |
|---|---|---|
| `sysio.roa` | ~157 MB, fixed | The contract's own rows: policies, reslimits, node-owner records |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
```

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
| 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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's total_cpu_weight (or total_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 privileged setalimits / setacctcpu / setacctnet can add weight outside ROA, and bootstrap does.

Comment thread docs/roa-overview.md Outdated
| `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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
## 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated

## Reference

### Actions on `sysio.roa`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@heifner

heifner commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in e365f2e2f7, and one finding split out as a code fix.

#590get_required_keys hard-coded <payer>@active while check_authorization pairs the marker with any real permission, so self-pay under owner or a linked custom permission was valid at consensus and unsignable through the endpoint every wallet calls. It removes the special case rather than duplicating the pairing rule, carries a regression test verified against a negative control, and the full 1516-case unit_test suite is green. That should merge first; this page assumes it.

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.

@heifner
heifner requested a review from huangminghuang August 26, 2026 15:57

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md
| 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md

### Why the contract can afford it

The executable system contracts — `sysio.token`, `sysio.msig`, `sysio.wrap` and the rest — carry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 setsyscodegiftram 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md

Two anchors to calibrate against:

- **NET is small and predictable, but it is not just the action.** Each action is billed its own

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
**`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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@heifner
heifner requested a review from huangminghuang August 26, 2026 19:26
@heifner
heifner requested a review from huangminghuang August 31, 2026 15:20

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +225 to +227
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +711 to +713
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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@heifner
heifner requested a review from huangminghuang August 31, 2026 16:48

@huangminghuang huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +29 to +34
**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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +159 to +160
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +161 to +163
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Comment thread docs/roa-overview.md Outdated
Comment on lines +712 to +714
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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +29 to +34
**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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated

| | EOS staking | REX | PowerUp | **Wire ROA** |
|---|---|---|---|---|
| Who is billed for a transaction | Signer | Signer | Signer | **Called contract, unless the action names an explicit payer** |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +109 to +111
**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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +35 to +36
[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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and that was my wording from the last round. Scoped to "takes the contract's own CPU and NET out of the picture."

Comment thread docs/roa-overview.md Outdated
Comment on lines +648 to +653
> **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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +155 to +159
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."]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).”

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +638 to +642
> `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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +137 to +138
You can only volunteer yourself, or someone who co-signs. An account that names itself payer needs
its own allocation and fails without one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +153 to +159
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."]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +198 to +214
**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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
Comment on lines +249 to +250
For a **user** account, consensus never consults those zeros. It signs, the contract pays, the
transaction succeeds.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.payer selects 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 setsyscodegiftram establishes 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.

Comment thread docs/roa-overview.md Outdated

```mermaid
flowchart TD
A["Regular action arrives"] --> B{"authorization[0].permission<br/>== sysio.payer ?"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/roa-overview.md Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 setsyscodegiftram. 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.

Comment thread docs/roa-overview.md Outdated
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."]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.payer marker 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 setsyscodegiftram later 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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() calls verify_account_ram_usage for 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 huangminghuang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved per the latest review. The documentation changes are ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants