Skip to content

feat(server,cli): support PGAS allowances - #391

Open
TarikGul wants to merge 7 commits into
mainfrom
tg/pgas-allowance
Open

feat(server,cli): support PGAS allowances#391
TarikGul wants to merge 7 commits into
mainfrom
tg/pgas-allowance

Conversation

@TarikGul

Copy link
Copy Markdown
Member

SmartContractAllowance was protocol-only: both allocation paths answered NotAvailable regardless of what the host could do. They now claim PGAS on Asset Hub, through one allocator shared by the direct and SSO paths.

Shape

Two chains take part. Membership and its ring revision come from the People chain; the claim is submitted on Asset Hub, which learns about People's rings through MembersSubscriber. A proof can only be verified against a revision Asset Hub has already imported, so the flow waits for it rather than submitting something the
runtime cannot check. Asset Hub keeps only its most recent roots per ring, so a revision it does not hold is either still arriving or gone for good, await_ring_revision tells those apart, because the first is worth waiting for and the second means rebuilding the proof against a newer ring.

PGAS differs from the other two allowances in what it credits. The statement-store and Bulletin allowances derive a dedicated //allowance//… account and hand its key back; PGAS pre-warms a balance on the product account the request names, so it takes a DerivationIndex, credits product_keypair, and returns nothing. That matches SsoAllocatedResource::SmartContractAllowance, which already carried no key material.

Asset Hub is resolved through the host's chain set rather than a configured hash. features::genesis_for answers which genesis the host serves for a role, and chain_info now sits on it so the product-facing get_chain_info and in-core callers cannot disagree about what the host serves.

Groundwork

build_proof_message, build_unsigned_extrinsic, and the two info-variant lookups each hardcoded AsResources; they now take the extension identifier, with the AsResources forms as thin wrappers. The field-enum lookup changed shape rather than gaining a parameter — it located the collection by the type name MembershipCollection, which AsPgas does not have (it names its own PgasCollection), so it now finds the enum by the variant it contains. Both generalized builders read the transaction-extension version from metadata, so the second authorizing extension encodes for the pipeline the runtime declares.

A routing bug fixed on the way

The Asset Hub entry in the CLI's live_chain_endpoints carried bf0488db… while the chain reports 23e730eb…. Routing matches on that hash, so a host asking for Asset Hub silently reached the People chain instead, and
ChainContextCache only warns on genesis divergence — the claim would then have run against People metadata under an Asset Hub cache key. Corrected to the value the chain reports.

Scope

Part of #334, covering "Make sure that PGAS allowances are supported".

No host currently returns an Asset Hub entry from supported_chains — the CLI errors, the JS and iOS bridges return empty sets, and dotli does not implement it (though it already has the Asset Hub config and connect path). So this answers NotAvailable in production until that lands, which is a follow-up in the host repos. The claim itself is verified below.

Verification

Offline: the alias context and spent-alias key layouts, the five-field AsPgas::Claim payload, the shared lookups against their AsResources wrappers, and the chain-set resolver. A test polls a PGAS allocation once and requires it to be waiting on the Asset Hub connection, asserting the outcome alone cannot distinguish a real claim from the previous stub, since both answer NotAvailable.

Live against paseo Asset Hub (next-asset-hub-paseo, spec 2000036) and the People chain. Before writing anything, every on-chain assumption was checked: AsPgas::Claim declares exactly the five fields encoded here, Pgas is pallet 99 with claim_pgas(u32, AccountId32), both storage keys hash as expected, and MaxClaimsPerPeriodPerLitePerson is 40. tests/live_asset_hub.rs then covers the runtime serving V16, RingCommitmentRecord decoding the real RingRoots value, and a long-pruned revision reporting as pruned rather than waiting out the timeout.

A claim was submitted for day 20678 and took slot 0; the next scan advanced to slot 1, and a second claim took it. The claim retries against a different slot on a duplicate submission, as the statement-store and long-term-storage claims do and the flow this was ported from did not.

pgas-check mirrors alloc-check for this path and connects to both chains directly: the host's ChainProvider drops Asset Hub unless E2E_LIVE_CHAIN=1 and then falls back to the People chain, so a claim routed through it would fail for reasons unrelated to the claim.

…tension

`build_unsigned_extrinsic`, `build_proof_message`, and the info-variant lookup
each hardcoded `AsResources`. PGAS authorizes through `AsPgas` on Asset Hub and
needs the same three, so they now take the extension identifier and the
`AsResources` forms are thin wrappers over them.

The field-enum lookup changed shape rather than gaining a parameter. It located
the collection by type name, `MembershipCollection`, which is specific to
`AsResources`; `AsPgas` names its own `PgasCollection`. It now locates the field
enum by the variant it contains, since the membership tier inside them is named
the same either way. `MissingMembershipCollection` and
`MissingLitePeopleCollection` give way to one `MissingExtensionFieldVariant` that
names the extension, the info variant, and the tier it looked for.

Both generalized builders read the transaction-extension version from metadata, so
a second authorizing extension encodes for the pipeline the runtime declares
rather than repeating the constant that was removed from the `AsResources` path.

Tests pin the wrappers against the general forms, so the pair cannot drift, and
cover an unknown extension, info variant, and nested variant.
…zation

The claim's chain-independent half: the ring-VRF alias context, the spent-alias
storage key, the per-day slot scan, the `Pgas.claim_pgas` call, and the `AsPgas`
extension `extra`.

Each piece sits beside the statement-store and long-term-storage equivalent it
mirrors rather than in a module of its own, so the alias helper reuses
`SlotError::AliasInContext`, the storage key reuses `blake2_128_concat`, and the
extension `extra` reuses `OPTION_SOME` and the identifier-parameterized builders.

Two layouts differ from their siblings and are deliberate. The PGAS alias context
writes day and slot little-endian where the other two are big-endian, because the
mobile wallet writes them that way and the runtime verifies the proof against the
same bytes. `AsPgas` names its membership enum `PgasCollection`, so the tier is
resolved by variant name rather than by the `MembershipCollection` type name.

The slot model follows long-term storage, not the statement store:
`Pgas.ClaimedGasAliases` records a spent alias with a unit value, so presence is
the whole answer and no entry is decoded.

Verified against live Asset Hub (`next-asset-hub-paseo`, spec 2000036) before
writing: `AsPgas::Claim` declares exactly the five fields encoded here, `Pgas` is
pallet 99 with `claim_pgas(u32, AccountId32)`, `ClaimedGasAliases` hashes as
`Identity(u32be day)` then `Blake2_128Concat(alias)`, and
`MaxClaimsPerPeriodPerLitePerson` is 40. A test pins the five-field payload, since
a short payload is accepted locally and then panics the runtime in
`validate_transaction`.
The claim orchestrator. Two chains take part: membership and its ring revision
come from the People chain, the claim is submitted on Asset Hub, and Asset Hub
learns about People's rings through `MembersSubscriber`.

That propagation is the part worth care. Asset Hub keeps only its most recent
roots per ring, so a revision it does not hold is either still arriving or gone
for good, and those need different answers — the first is worth waiting for, the
second means the proof must be rebuilt against a newer ring.
`await_ring_revision` distinguishes them and is public, so a host can check
propagation before offering a claim.

The claim retries against a different slot on a duplicate submission, which the
statement-store and long-term-storage claims already do and the flow this was
ported from did not: two hosts racing the same day both scan a free slot before
either lands.

`Metadata::extension_info_field_count` joins the other three lookups in taking
the extension identifier, so the arity guard covers `AsPgas` as well as
`AsResources`.

Verified against live Asset Hub (`next-asset-hub-paseo`, spec 2000036) and the
live People chain, in `tests/live_asset_hub.rs`: the runtime serves V16 and
declares five fields for `AsPgas::Claim`; `RingCommitmentRecord` decodes the real
`RingRoots` value, where People's current ring 2 revision is among the roots Asset
Hub holds; and a long-pruned revision reports as pruned instead of waiting out the
timeout. Those tests connect directly rather than through the host's
`ChainProvider`, which filters Asset Hub out unless `E2E_LIVE_CHAIN=1` and then
silently falls back to the People chain.
`pgas-check` mirrors `alloc-check` for the PGAS path: it reports ring membership
on the People chain, whether Asset Hub has imported that ring's revision, the
day's first unclaimed slot, and with `--submit` performs the claim.

It connects to both chains directly rather than through the host's
`ChainProvider`. That provider drops Asset Hub unless `E2E_LIVE_CHAIN=1` and then
falls back to the People chain for any unrecognised genesis, so a claim routed
through it would run against People metadata and fail for reasons that have
nothing to do with the claim.

The preset gains `asset_hub_ws` and `asset_hub_genesis`, and the Asset Hub entry
in `live_chain_endpoints` now carries the genesis the chain actually reports.
The previous value predates a testnet redeployment, and a wrong hash there is
worse than a stale one elsewhere: routing matches on it, so a host asking for
Asset Hub silently reached the People chain instead.

Verified against paseo: a claim was submitted for day 20678, took slot 0, and the
next scan advanced to slot 1, so the spent-alias accounting holds. A second claim
took slot 1.
…ailable

`SmartContractAllowance` was protocol-only: both allocation paths answered
`NotAvailable` regardless of what the host could do. They now claim, through one
allocator shared by the direct and SSO paths.

PGAS differs from the other two allowances in what it credits. The statement-store
and Bulletin allowances derive a dedicated `//allowance//…` account and hand its
key back; PGAS pre-warms a balance on the product account the request names, so it
takes a `DerivationIndex`, credits `product_keypair`, and returns nothing. That
matches `SsoAllocatedResource::SmartContractAllowance`, which already carried no
key material.

Asset Hub is resolved through the host's chain set rather than a configured hash.
`features::genesis_for` answers which genesis the host serves for a role, and
`chain_info` now sits on it so the product-facing method and in-core callers cannot
disagree. A host that serves no Asset Hub reports that plainly, which is better
than reaching whatever chain a stale hash happens to route to. `ChainRpcClient`
replaces `BulletinRpcClient`: two chains use it now and its `context` field already
said which.

No host currently returns an Asset Hub entry, so this answers `NotAvailable` in
production until they do — the claim itself is verified against paseo.

The batch test asserted `SmartContractAllowance => NotAvailable`, which the stub
made true for free. It now fails the connect deliberately, so what it pins is that
one resource failing does not poison the others. Asserting the outcome alone cannot
distinguish a real claim from the old stub, so a second test polls the allocation
once and requires it to be waiting on the Asset Hub connection.
@TarikGul
TarikGul requested a review from a team August 13, 2026 17:35
@TarikGul TarikGul changed the title feat(server,cli): claim Asset Hub PGAS allowances feat(server,cli): support PGAS allowances Aug 13, 2026
TarikGul and others added 2 commits August 13, 2026 13:57
`ChainNotServed` and `SupportedChains` are only constructed by the native
allocator, so on `wasm32-unknown-unknown` they are unreachable variants. CI checks
that target with `-D warnings`, which makes an unconstructed variant an error.

Both are now gated to native, as `ProductAccount` in the same enum already is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant