Period-based authorizations#469
Closed
franciscoaguirre wants to merge 28 commits into
Closed
Conversation
This extension gives `store` transactions a proportional priority boost based on the already consumed allowance.
Introduces a chain-wide cap on bytes committed to permanent storage (via `renew`) so each runtime can express its own permanent-storage budget. The bulletin-westend runtime sets it to 1.7 TiB, the test mock leaves it unbounded.
Re-authorizing an unexpired account/preimage replaces `bytes_allowance` rather than adding to it. Three tests assumed the additive form. Renamed `authorize_account_does_not_push_expiry` to `re_authorize_account_replaces_allowance_and_keeps_expiry` to reflect both invariants it now covers.
…ko-authorizations
…ko-authorizations
`AllowanceBasedPriority` is now generic over a `BoostStrategy` so the runtime can pick its policy in the `TxExtension` tuple. Two impls ship: - `ProportionalBoost`: original PR #448 behaviour — linear in remaining allowance. - `FlatBoost`: constant boost while in-budget, `0` once over-budget. The proportional form is vulnerable to censorship: a fresh-allowance signer outranks partly-used ones and can starve them with small-tx spam (see karolk91's analysis on PR #448). `FlatBoost` makes in-budget signers all rank equal and over-budget ones strictly lower; pool nonce/ arrival ordering breaks ties. Both runtimes are wired to `FlatBoost`. Three pallet unit tests (`proportional_scales_with_remaining_allowance`, `flat_is_constant_while_in_budget`, `flat_does_not_let_fresh_outrank_partly_used`) document the contracts and the censorship-relevant difference.
…ko-authorizations
…ko-authorizations
bkontur
added a commit
that referenced
this pull request
Apr 29, 2026
Brings just the AuthorizationExtent + boost-strategy parts of #469; the period / two-slot / bytes_permanent surface is left out. - AuthorizationExtent: gain transactions_used / transactions_allowance fields. authorize_account now uses its 3rd parameter to set/extend transactions_allowance; check_authorization saturating-bumps transactions_used on consume. - BoostStrategy: a shared in_budget helper gates both the byte and tx axes; FlatBoost is in-budget at-cap (matching #469); ProportionalBoost scales by the tighter of the two remainders. - AllowanceBasedPriority::validate increments transactions_used in the post-this-tx extent before computing the boost. - v1->v2 migration carries the old transaction-count field over to the new transactions_allowance. - Genesis config tuple becomes (account, transactions_allowance, bytes_allowance); preimage authorize_* always sets transactions_allowance = 1. - Tests + bulletin-{westend,paseo} runtime tests updated for the new expected counters; new boost_tests::tx_axis_gates_boost_independently. Out of scope (per #469): bytes_permanent, the two-slot current/next grant model, for_period parameter, renew matching in the boost extension, the People-Chain-aligned period model.
4 tasks
bkontur
added a commit
that referenced
this pull request
Apr 29, 2026
Brings just the AuthorizationExtent + boost-strategy parts of #469; the period / two-slot / bytes_permanent surface is left out. - AuthorizationExtent: gain transactions_used / transactions_allowance fields. authorize_account now uses its 3rd parameter to set/extend transactions_allowance; check_authorization saturating-bumps transactions_used on consume. - BoostStrategy: a shared in_budget helper gates both the byte and tx axes; FlatBoost is in-budget at-cap (matching #469); ProportionalBoost scales by the tighter of the two remainders. - AllowanceBasedPriority::validate increments transactions_used in the post-this-tx extent before computing the boost. - v1->v2 migration carries the old transaction-count field over to the new transactions_allowance. - Genesis config tuple becomes (account, transactions_allowance, bytes_allowance); preimage authorize_* always sets transactions_allowance = 1. - Tests + bulletin-{westend,paseo} runtime tests updated for the new expected counters; new boost_tests::tx_axis_gates_boost_independently. Out of scope (per #469): bytes_permanent, the two-slot current/next grant model, for_period parameter, renew matching in the boost extension, the People-Chain-aligned period model. Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
bkontur
added a commit
that referenced
this pull request
Apr 30, 2026
* Adds `AllowanceBasedPriority` extension This extension gives `store` transactions a proportional priority boost based on the already consumed allowance. * Nit plus test * Do not degrade user's performance in time - reset usage when refresh_authorization * Migration for Paseo * Fix migration * fmt * Ping CI * Fix tests to match assignment semantics of `authorize` Re-authorizing an unexpired account/preimage replaces `bytes_allowance` rather than adding to it. Three tests assumed the additive form. Renamed `authorize_account_does_not_push_expiry` to `re_authorize_account_replaces_allowance_and_keeps_expiry` to reflect both invariants it now covers. * Compilation for paseo after main merge * Doc nit for refresh_authorization * Migration for paseo, which was merged meantime * Refactor allowance boost behind `BoostStrategy`, default to `FlatBoost` `AllowanceBasedPriority` is now generic over a `BoostStrategy` so the runtime can pick its policy in the `TxExtension` tuple. Two impls ship: - `ProportionalBoost`: original PR #448 behaviour — linear in remaining allowance. - `FlatBoost`: constant boost while in-budget, `0` once over-budget. The proportional form is vulnerable to censorship: a fresh-allowance signer outranks partly-used ones and can starve them with small-tx spam (see karolk91's analysis on PR #448). `FlatBoost` makes in-budget signers all rank equal and over-budget ones strictly lower; pool nonce/ arrival ordering breaks ties. Both runtimes are wired to `FlatBoost`. Three pallet unit tests (`proportional_scales_with_remaining_allowance`, `flat_is_constant_while_in_budget`, `flat_does_not_let_fresh_outrank_partly_used`) document the contracts and the censorship-relevant difference. * Docs nits * Compute allowance boost against post-this-tx state * Minimalistic alignment with paritytech/individuality#785 (part 1) * Pick transactions_used / transactions_allowance from #469 Brings just the AuthorizationExtent + boost-strategy parts of #469; the period / two-slot / bytes_permanent surface is left out. - AuthorizationExtent: gain transactions_used / transactions_allowance fields. authorize_account now uses its 3rd parameter to set/extend transactions_allowance; check_authorization saturating-bumps transactions_used on consume. - BoostStrategy: a shared in_budget helper gates both the byte and tx axes; FlatBoost is in-budget at-cap (matching #469); ProportionalBoost scales by the tighter of the two remainders. - AllowanceBasedPriority::validate increments transactions_used in the post-this-tx extent before computing the boost. - v1->v2 migration carries the old transaction-count field over to the new transactions_allowance. - Genesis config tuple becomes (account, transactions_allowance, bytes_allowance); preimage authorize_* always sets transactions_allowance = 1. - Tests + bulletin-{westend,paseo} runtime tests updated for the new expected counters; new boost_tests::tx_axis_gates_boost_independently. Out of scope (per #469): bytes_permanent, the two-slot current/next grant model, for_period parameter, renew matching in the boost extension, the People-Chain-aligned period model. Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> * transactions_used -> transactions * Refresh: only extend expiry, do not reset consumed counters Per review feedback (#448 r3163274071): resetting `bytes` and `transactions` on refresh implicitly grants a fresh boost-tier window, which conflates two operations under one extrinsic. Refresh now does exactly what its name says — extend the expiration block. Holders who want more capacity call `authorize_account` (additive on the unexpired path). Doc updates on `refresh_account_authorization`, `refresh_preimage_authorization` and the `refresh_authorization` helper make the split explicit. `authorize_account`'s doc also gets updated to describe the new tx-axis additivity. * Move back to 14 * Tighten priority constants for store / renew - `AuthorizationPeriod` 90 days -> 14 days, aligning with the `LongTermStoragePeriodDuration` (2 weeks) on the People-Chain side. - Drop the unused `SudoPriority` and `SetPurgeKeysPriority` constants; they were only chained into `RemoveExpiredAuthorizationPriority` and not wired anywhere else. - `RemoveExpiredAuthorizationPriority` is now `TransactionPriority::MAX` so permissionless cleanups always outrank stores. - `StoreRenewPriority` is `TransactionPriority::MAX / 4`. With `AllowanceBasedPriority` adding `ALLOWANCE_PRIORITY_BOOST` for in-budget signers, in-budget txs land just above over-budget ones without saturating `u64` and with plenty of headroom both above generic transactions and below the cleanup ceiling. * Nit * More nits --------- Co-authored-by: Branislav Kontur <bkontur@gmail.com> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
bkontur
added a commit
that referenced
this pull request
May 5, 2026
* Adds `AllowanceBasedPriority` extension This extension gives `store` transactions a proportional priority boost based on the already consumed allowance. * Nit plus test * Do not degrade user's performance in time - reset usage when refresh_authorization * Migration for Paseo * Fix migration * fmt * Ping CI * Adjust `AuthorizationExtent` to keep bytes/bytes_permanent/bytes_allowance * Add MaxPermanentStorageSize Config bound Introduces a chain-wide cap on bytes committed to permanent storage (via `renew`) so each runtime can express its own permanent-storage budget. The bulletin-westend runtime sets it to 1.7 TiB, the test mock leaves it unbounded. * Fix tests to match assignment semantics of `authorize` Re-authorizing an unexpired account/preimage replaces `bytes_allowance` rather than adding to it. Three tests assumed the additive form. Renamed `authorize_account_does_not_push_expiry` to `re_authorize_account_replaces_allowance_and_keeps_expiry` to reflect both invariants it now covers. * Compilation for paseo after main merge * Doc nit for refresh_authorization * Migration for paseo, which was merged meantime * Refactor allowance boost behind `BoostStrategy`, default to `FlatBoost` `AllowanceBasedPriority` is now generic over a `BoostStrategy` so the runtime can pick its policy in the `TxExtension` tuple. Two impls ship: - `ProportionalBoost`: original PR #448 behaviour — linear in remaining allowance. - `FlatBoost`: constant boost while in-budget, `0` once over-budget. The proportional form is vulnerable to censorship: a fresh-allowance signer outranks partly-used ones and can starve them with small-tx spam (see karolk91's analysis on PR #448). `FlatBoost` makes in-budget signers all rank equal and over-budget ones strictly lower; pool nonce/ arrival ordering breaks ties. Both runtimes are wired to `FlatBoost`. Three pallet unit tests (`proportional_scales_with_remaining_allowance`, `flat_is_constant_while_in_budget`, `flat_does_not_let_fresh_outrank_partly_used`) document the contracts and the censorship-relevant difference. * Docs nits * Compute allowance boost against post-this-tx state * Compute allowance boost against post-this-tx state * Updated soft allowance design * Updated hard allowance design and renewals (draft, TODO: challange with auto-renewal) * Soft-side lifecycle: preserve bytes_permanent across re-authorize and remove_expired Two paired rules from the authorizations design were missing in code: 1. `authorize_account` on an expired-but-present entry now re-grants the cap and resets `bytes` only — it preserves `bytes_permanent`. Previously the expired branch overwrote the whole extent with a fresh one, zeroing `bytes_permanent` and silently bypassing the per-account hard cap on the next renew cycle. 2. `remove_expired_authorization` now refuses with `Error::AuthorizationHasPermanentStorage` while `bytes_permanent > 0`. Removing the entry would orphan the (lazy) ledger drain — its decrement would have nowhere to land. The entry becomes removable once `bytes_permanent` has dropped back to `0` naturally. Together these guarantee the ledger drain (introduced when the hard-side work lands) is the only path that ever decrements `bytes_permanent`. No other code path can clobber or orphan the counter. Tests cover both rules: `authorize_account_after_expiry_preserves_bytes_permanent` and `remove_expired_account_authorization_refuses_while_bytes_permanent_outstanding`. * paseo: back MaxPermanentStorageSize with `parameter_types! { pub storage }` Replaces the compile-time `MAX_PERMANENT_STORAGE_SIZE` const with a storage-backed `parameter_types!` entry. The Config trait constant is unchanged on the pallet side; the runtime just chooses a mutable backing, so governance (root) can raise or lower the cap at runtime via `system.set_storage` without a runtime upgrade. Initial value is still 1.7 TiB, seeded on first read. * WIP: hard cap (very draft) * Minimalistic alignment with paritytech/individuality#785 (part 1) * Pick transactions_used / transactions_allowance from #469 Brings just the AuthorizationExtent + boost-strategy parts of #469; the period / two-slot / bytes_permanent surface is left out. - AuthorizationExtent: gain transactions_used / transactions_allowance fields. authorize_account now uses its 3rd parameter to set/extend transactions_allowance; check_authorization saturating-bumps transactions_used on consume. - BoostStrategy: a shared in_budget helper gates both the byte and tx axes; FlatBoost is in-budget at-cap (matching #469); ProportionalBoost scales by the tighter of the two remainders. - AllowanceBasedPriority::validate increments transactions_used in the post-this-tx extent before computing the boost. - v1->v2 migration carries the old transaction-count field over to the new transactions_allowance. - Genesis config tuple becomes (account, transactions_allowance, bytes_allowance); preimage authorize_* always sets transactions_allowance = 1. - Tests + bulletin-{westend,paseo} runtime tests updated for the new expected counters; new boost_tests::tx_axis_gates_boost_independently. Out of scope (per #469): bytes_permanent, the two-slot current/next grant model, for_period parameter, renew matching in the boost extension, the People-Chain-aligned period model. Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com> * transactions_used -> transactions * Refresh: only extend expiry, do not reset consumed counters Per review feedback (#448 r3163274071): resetting `bytes` and `transactions` on refresh implicitly grants a fresh boost-tier window, which conflates two operations under one extrinsic. Refresh now does exactly what its name says — extend the expiration block. Holders who want more capacity call `authorize_account` (additive on the unexpired path). Doc updates on `refresh_account_authorization`, `refresh_preimage_authorization` and the `refresh_authorization` helper make the split explicit. `authorize_account`'s doc also gets updated to describe the new tx-axis additivity. * Move back to 14 * Tighten priority constants for store / renew - `AuthorizationPeriod` 90 days -> 14 days, aligning with the `LongTermStoragePeriodDuration` (2 weeks) on the People-Chain side. - Drop the unused `SudoPriority` and `SetPurgeKeysPriority` constants; they were only chained into `RemoveExpiredAuthorizationPriority` and not wired anywhere else. - `RemoveExpiredAuthorizationPriority` is now `TransactionPriority::MAX` so permissionless cleanups always outrank stores. - `StoreRenewPriority` is `TransactionPriority::MAX / 4`. With `AllowanceBasedPriority` adding `ALLOWANCE_PRIORITY_BOOST` for in-budget signers, in-budget txs land just above over-budget ones without saturating `u64` and with plenty of headroom both above generic transactions and below the cleanup ceiling. * Nit * More nits * Update design * Nit * Hard-side hardening: try_state invariants, validate-time guard, capacity events - Doc: Hard Limit header reflects implementation (was "Proposed; not yet wired"). - try_state: assert PermanentStorageUsed == Σ bytes_permanent across Authorizations == Σ size across PermanentStorageLedger entries; cursor <= current_block + 1; no ledger entries before cursor; used <= MaxPermanentStorageSize. - check_authorization_expired: reject remove_expired_* at validate when bytes_permanent > 0 (new AUTHORIZATION_HAS_PERMANENT_STORAGE = Custom(7)), mirrors the dispatch-time guard so pool ingress doesn't accept soon-to-fail txs. - Events: PermanentStorageUsedUpdated { used } on every renew bump and every drain decrement; PermanentStorageNearCap { used, cap } on rising-edge crossing of PERMANENT_STORAGE_NEAR_CAP_PERCENT (80%). Single helper update_permanent_storage_used owns the read, write, and event emission. 9 new tests cover: remove_expired validate guard (account + preimage); renew/drain event emission; rising-edge near-cap behaviour; happy-path + 4 corruption variants of the new try_state invariants. * Add note * Clippy a nits * Nit for bytes_permanent * nit * Simplify renewal hard cap (#486) * feat: simplify * Fix migration * Drop expired authorizations --------- Co-authored-by: Branislav Kontur <bkontur@gmail.com> * Nit * More nits * Fix migration? --------- Co-authored-by: Bastian Köcher <info@kchr.de> Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Contributor
Author
|
Closing in favor of #509 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Created on top of
bko-authorizationsto align both issues: no gaps in authorizations and unlimited low-prioritystore()transactions.Background
Authorizations on Bulletin used to be block-number-based (
AuthorizationPeriod = 90 days) and decoupled from the People-Chain claim period. The recent soft-cap PR madestorenot waste blockspace, butrenewbecame unbounded andbytes_permanentgrew across refreshes without ever decrementing — disk burden was no longer pinned by anything.This PR re-aligns Bulletin's authorizations with the People-Chain claim period, swaps the per-account block expiry for a two-slot (current, next) period grant, and finalises the soft-store / hard-renew / tx-quota accounting. Refresh extrinsics are gone; period rollover does the work.
How a holder uses the chain (Alice, 14-day period, 10 MiB / 100 tx)
1. Basic period — soft cap, blockspace never wasted
Alice claims long-term storage on People for the current period P; People sends
authorize_account(Alice, 100, 10 MiB, P)to Bulletin via XCM. Throughout P:store(8 MiB)→ in-budget. Top tier (base + ALLOWANCE_PRIORITY_BOOST). State:bytes = 8 MiB,tx_used = 1.store(3 MiB)→ combined 11 MiB > 10 MiB. Bottom tier (base, no boost). The tx still validates and rides leftover blockspace;bytessaturates to 11 MiB;tx_usedstays at 1 because over-budget stores don't burn tx slots.Alice can keep submitting low-priority stores indefinitely after her allowance is exhausted — they only land when there's spare blockspace. Blocks never sit empty because of allowance enforcement, and the bound on total temp storage comes from chain physics (
MaxBlockBytes × RetentionPeriod ≈ 1.7 TiB), not from a per-tx rejection.2. Two-slot model + forward claim
Alice wants continuous coverage across the period boundary so auto-renewals (see §4) survive. While still in P, she submits a second People-Chain claim with
period = P + 1. The People predicate now acceptscurrent + 1; the XCM lands asauthorize_account(Alice, 100, 10 MiB, P + 1)on Bulletin, which routes it into thenextslot:When the clock crosses into P + 1, the next call against Alice's authorization runs prune-and-shift:
current(P) is expired and dropped,next(P + 1) is promoted intocurrent. Both slots' counters start fresh at zero — no refresh extrinsic, nobytes_permanentcarry-over.If Alice forgets to forward-claim, period rollover leaves
current = None.storeandrenewreject; her blobs lapse after theirRetentionPeriod. Stale entries are removed by the existingremove_expired_account_authorizationpermissionless call.3. Multiple claims in the same period add
A credential entitles its holder to N claims per period, each granting 1/N of the credential's per-period maximum. (E.g., PoP with N = 4 might give 4 claims of 2.5 MiB / 25 tx each, totalling 10 MiB / 100 tx per period.) The claims are independent calls of
claim_long_term_storage(period, counter, account_id), wherecounter ∈ 0..Nindexes the claim within the period. The holder can spend any subset, in any order, at any time during the period — People-Chain bookkeeping enforces that each(period, alias, counter)is spent at most once. PoP and PoP-Lite are mutually exclusive at the credential level; this mechanic stacks claims of the same credential, not different ones.Bulletin's
authorizeroutes every same-period claim into the same slot and adds. Worked example, Alice spends claims 0 and 1 of her four in period P:bytes_allowance = 2.5 MiB,transactions_allowance = 25bytes_allowance = 5 MiB,transactions_allowance = 50bytes,bytes_permanent,tx_used) are preserved across the second claim, so her in-period spending so far isn't erased by topping up.Two design properties matter here:
bytes_allowancerepeatedly.This is the only mechanic by which a slot's allowance grows mid-period; any other source (a different credential, an out-of-period claim) is rejected by People-Chain validation before it ever reaches Bulletin.
4. Renew is hard-capped — composes with data-renewal PR (#313)
renew(...)consumes from the same per-period budget asstore. The cap is a hard rejection on the combined byte counter and ontransactions_used:Worked example, period P + 1 starts with Alice's
nextslot promoted tocurrentwith a fresh 10 MiB / 100 tx grant:renew(8 MiB blob)store(2 MiB)renew(3 MiB other blob)store(5 MiB)Alice has full freedom to split her budget — keep 8 MiB alive AND store 2 MiB new at boost priority, AND throw 5 MiB more at low priority. She cannot force-renew past 10 MiB combined; that's what bounds disk burden under repeated auto-renewals.
Composition with the data-renewal PR: auto-renewal schedules
renew(block, index)on the holder's behalf shortly before each blob'sRetentionPeriodelapses. The renewal is subject to the hard cap above. The forward-claim mechanic from §2 is what makes auto-renewal correct across period boundaries: thenextslot is in place when the renewal fires, prune-and-shift promotes it, and the renewal lands. Holders who stop claiming see their auto-renewals start failing — their blobs lapse cleanly without leaving permanently-orphaned data behind.5. Transactions quota as DoS shield
Independently of the byte budget,
transactions_allowancecaps the number of boost-tier calls a holder can land per period. Why the boost tier specifically: a holder with 10 MiB / period byte allowance could otherwise land 10 M boost-priority 1-byte stores for free, occupyingMaxBlockTransactionsslots across blocks. The cap stops that.Alice with
transactions_allowance = 3, plenty of bytes:store(1 byte)×3store(1 byte)The 4th store still validates (soft cap on the boost — never rejection on store); it just lands as low priority and the tx counter doesn't tick. Alice can submit unlimited low-priority 1-byte stores after the boost budget is exhausted, bounded only by chain capacity. Renew, by contrast, is hard-rejected once
tx_used + 1 > tx_allowance, since it has no low-priority path.Failure mode if the holder stops engaging
nextexistscurrent = None→ renewals reject → blobs lapse oneRetentionPeriodafter their last renew. No silent revival.current. New stores work; previously-lapsed blobs are gone.Surface changes
AuthorizationExtentgainsbytes_permanent,transactions_used,transactions_allowance.Authorizationis now{ current: Option<PeriodGrant>, next: Option<PeriodGrant> }.Config: dropsAuthorizationPeriod; addsTimeProvider: UnixTimeandPeriodDuration: Get<u64>(must match People'sLongTermStoragePeriodDuration).authorize_account(who, transactions, bytes, for_period),authorize_preimage(content_hash, max_size, for_period).for_period ∈ {current, current + 1}. Same-slot re-authorize adds. Refresh extrinsics removed.AllowanceBasedPriorityboost matchesCall::renewtoo;BoostStrategy::boostconsultsbytes + bytes_permanentANDtx_usedagainst the post-this-tx state.is_accepted_long_term_storage_periodnow also acceptscurrent + 1;claim_long_term_storagethreadsfor_periodthrough, mapping grace claims to the current period before XCM.AllocateStoragetrait gainsfor_periodandcurrent_period(), dropsrefresh_allocation.v1 → v2migration translates old{transactions, bytes}single-grant into the new two-slot shape withcurrent.period = current_period_at_upgradeand used counters at zero.Tests
pallet-bulletin-transaction-storage: 36/36indiv-pallet-resources: 94/94indiv-pallet-proof-of-ink: 40/40bulletin-westend-runtime,bulletin-paseo-runtime,next-people-paseo-runtimeOut of scope
bp-bulletin-peopleprimitives crate forPeriodDuration. Currently inlined in both bulletin runtimes with TODO comments; alignment with People'sLongTermStoragePeriodDurationis by convention.