Skip to content

perf(server): stop re-reading metadata and rings on every allowance call - #366

Open
TarikGul wants to merge 5 commits into
mainfrom
tg/allowances-in-core
Open

perf(server): stop re-reading metadata and rings on every allowance call#366
TarikGul wants to merge 5 commits into
mainfrom
tg/allowances-in-core

Conversation

@TarikGul

Copy link
Copy Markdown
Member

What

Steady state for an allowance-gated product call (statement_create_proof_authorized,
preimage_submit) is one state_getRuntimeVersion plus the slot scan. A submission adds
the ring walk and the extrinsic. Neither re-reads metadata or the genesis hash while the
runtime is unchanged.

This matches allocate_bulletin_allowance, which already returns before opening a
People-chain connection when allowance is in place.

Changes

  • slot::find_allocated_slot — query form of scan_slot_excluding; ignores free slots, so
    a table occupied by other accounts reports "no slot held" rather than erroring. The
    Ignore policy scans with it first and resolves a ring only when a submission is
    required. Increase is unchanged.
  • statement_allowance::ChainContextCache on RuntimeServices — holds
    ChainContext { metadata, state } keyed by the caller's configured genesis hash,
    revalidated with one state_getRuntimeVersion. One entry per configured chain, so no
    eviction policy.
  • ChainState.genesis_hash comes from the chain, not the caller's constant: CheckGenesis
    is signed over it, and configured constants drift (paseo-next-v2's People and Asset Hub
    hashes are both stale today). Divergence is logged, not fatal.
  • tests/live_people_chain.rs — read-only live-chain checks, #[ignore]d so cargo test
    stays offline.
PR Conflict
#360 RFC-0024 sso_responder.rs
#344 rfc17-coinage-core statement_allowance.rs (mortality field on ChainState)
#308 statement-allowance-renewal sso_responder.rs
#323 headless-host-chat-coinage none

`allocate_statement_store_allowance` resolved a LitePeople ring before
scanning for a slot, so every `statement_create_proof_authorized` paid a
`Members.RingKeys` page walk plus `chain_getBlockHash`,
`state_getRuntimeVersion` and `CurrentRingIndex` — even when the account
already held a slot for the period and nothing needed submitting. The ring
is only ever used to build a proof for an extrinsic.

Scan for an existing slot first under the `Ignore` policy and return the
allowance secret when one is held, resolving the ring only when a
submission is actually required. This matches what
`allocate_bulletin_allowance` already does: it reads
`TransactionStorage.Authorizations` and returns before opening a
People-chain connection at all.

`slot::find_allocated_slot` is the query form of `scan_slot_excluding` — it
ignores free slots, so a table fully occupied by other accounts answers
"no slot held" rather than erroring with `NoFreeStatementStoreSlot`.

The `Increase` policy is untouched: it always wants an additional slot, so
it goes straight to ring resolution as before.

One semantic change worth calling out: an account that already holds a slot
no longer has to prove ring membership to receive its own allowance key.
Membership is what earns a slot, not what makes an already-granted slot
usable, so a user whose membership lapses mid-period keeps working until
the period rolls over instead of failing with
`MissingLitePeopleMembership`.
Both native allowance paths downloaded the full `state_getMetadata`
response on every call. For statement-store that download exists only to
read one constant — `Resources.LiteStmtStoreSlotsPerPeriod`, the slot count
the scan needs — so the steady state pulled the entire runtime metadata to
learn a single `u32`.

`MetadataCache` keys decoded metadata by genesis hash and revalidates it
with `state_getRuntimeVersion`, a small request that only misses across a
runtime upgrade. It lives on `RuntimeServices` next to the existing
preimage and statement caches, so it is shared by every product runtime
built from one host role, and is keyed rather than per-chain-wrapper so the
Asset Hub PGAS path can reuse it without a second cache.

Combined with scanning before ring resolution, an already-allocated
statement-store product call now costs one `state_getRuntimeVersion` plus
the slot scan, where it previously cost a metadata download, two
chain-state reads, a ring-index read and a `Members.RingKeys` page walk on
top of the same scan.

`StatementStoreRpc::genesis_hash` and the `RuntimeServices` field are both
gated to non-wasm targets: `statement_allowance` is native-only, so on wasm
the accessor would be dead code.
…hash

The metadata cache validated its entry with `state_getRuntimeVersion` and
then `fetch_chain_state` asked for the same runtime version again, so the
submission path made two identical requests. Metadata and `ChainState` are
both fixed for a given runtime, so one entry holds both: `MetadataCache`
becomes `ChainContextCache` returning `ChainContext { metadata, state }`,
and the allowance paths no longer call `fetch_chain_state` at all.

`fetch_chain_state` keeps its signature for `truapi-host-cli` and is now
composed from `fetch_genesis_hash` and `fetch_runtime_version`, the same two
requests in the same order.

Filling `ChainState` from the cache key also closes a gap: the genesis hash
baked into every allowance extrinsic came from `chain_getBlockHash(0)` on
whatever connection the host supplied, and nothing checked it against the
chain the caller asked for. A host that wires `connect()` to the wrong chain
produced extrinsics signed for that chain, failing with an opaque validity
error. `ChainContextCache::get` now compares the two and reports
`GenesisHashMismatch` before downloading metadata.

Per allowance-gated product call, the steady state is one
`state_getRuntimeVersion` plus the slot scan; a submission adds the ring
walk and the extrinsic. Neither re-reads metadata or the genesis hash while
the runtime is unchanged.
Records why the cache needs no eviction policy (one entry per configured
chain, not per call) and corrects the crate README, which said the allowance
paths need no metadata.
Testing against the live paseo-next-v2 People chain showed the previous
commit's genesis check was wrong in both directions.

`network.rs` configures the People genesis as `c5af1826…`, but the chain
reports `89a63b11…`; Asset Hub has likewise diverged (`bf0488db…`
configured, `23e730eb…` live). Only Bulletin still matches. The testnet was
wiped and the constants were never refreshed — the exact failure RFC-0026
exists to remove.

So a divergence does not mean the host connected to the wrong chain, and
rejecting it broke the allowance path on a network where it had been
working. It also had the polarity backwards: `CheckGenesis` is signed over
`ChainState.genesis_hash`, so the value must come from the chain. Using the
configured constant would have produced extrinsics no node accepts.

`ChainContextCache::get` now keys entries by the caller's constant, since
that is the identity it routes connections by, and fills `ChainState` from
what the chain reports. A divergence is logged rather than fatal.

That split also exposed a cache bug the live run confirmed: the insert keyed
by the reported hash while the lookup used the configured one, so on any
network with a stale constant the cache missed on every call — silently
undoing the previous commit. `a_stale_configured_genesis_still_keys_the_cache`
covers it.

`tests/live_people_chain.rs` holds the checks that found this, `#[ignore]`d
so `cargo test` stays offline:

  cargo +nightly test -p truapi-host-cli --test live_people_chain \
    -- --ignored --nocapture

They confirm the reported genesis reaches `ChainState`, that a second read
hits the cache (`Arc::ptr_eq` on the metadata), that `find_allocated_slot`
scans a live period without erroring, and that live spec 1000032 still
exposes the `AsResources` variant indices the offline fixture pins at spec
1000000 — (2, 1) and (3, 1) in both.
@TarikGul
TarikGul requested a review from a team August 11, 2026 19:26
@TarikGul
TarikGul force-pushed the tg/allowances-in-core branch from fc8b9b5 to 9ab10b6 Compare August 11, 2026 19:29
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