Skip to content

Support for 3 async cores - #131

Open
ilchu wants to merge 50 commits into
devfrom
ic/2s-blocks-3-cores
Open

Support for 3 async cores#131
ilchu wants to merge 50 commits into
devfrom
ic/2s-blocks-3-cores

Conversation

@ilchu

@ilchu ilchu commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Switches both runtimes from 6-second to 2-second blocks with 3 relay cores per parachain, matching what polkadot-bulletin-chain PRs #417 (slot-based authoring) and #231 (2 s + 3 cores) did for Bulletin.

Stacked on #268 (relay-chain block denomination, #233). That ordering makes this PR a pure throughput/consensus change: every storage-pallet timeout, expiry and checkpoint window is denominated in relay blocks and reads its clock from RelaychainDataProvider, so nothing here touches storage timing at all — the constants keep their wall-clock meaning at any parachain block time.

Closes #113.

Runtime changes (runtimes/web3-storage-local/, runtimes/web3-storage-paseo/)

  • MILLISECS_PER_BLOCK: 6000 → 2000 (SLOT_DURATION follows)
  • BLOCK_PROCESSING_VELOCITY: 1 → 3 (three parachain blocks per relay slot)
  • New RELAY_PARENT_OFFSET = 1, wired into cumulus_pallet_parachain_system::Config::RelayParentOffset and the RelayParentOffsetApi runtime impl — required by slot-based authoring
  • UNINCLUDED_SEGMENT_CAPACITY derived from those two: (3 + RELAY_PARENT_OFFSET) * BLOCK_PROCESSING_VELOCITY = 12
  • RELAY_CHAIN_SLOT_DURATION_MILLIS stays 6000 (the relay sets its own slot time; it is also what the relay_time constants module from Measure pallet timeouts in relay chain blocks, not parachain blocks #268 anchors to, which is why storage timing is untouched)
  • spec_version: local runtime → 4, paseo → 4_004 (both sides of the Measure pallet timeouts in relay chain blocks, not parachain blocks #268 merge had bumped independently, so the merge takes the union)
  • scripts/runtimes-matrix.json: try-runtime --blocktime 6000 → 2000

pallet_aura::AllowMultipleBlocksPerSlot = true, FixedVelocityConsensusHook, and the RelayParentOffsetApi shell were already in place — they just needed the new constants threaded through.

Zombienet topology (zombienet.toml, zombienet/storage-paseo-local.toml)

3-core scheduling needs more validators and explicit core capacity: 6 relay validators (alice…ferdie), relay genesis patched with scheduler_params.num_cores = 3 and async_backing_params { allowed_ancestry_len = 6, max_candidate_depth = 6 }, num_cores = 3 on the parachain block, and two collators (alice, bob) running --authoring=slot-based.

examples/papi/assign-cores.ts + just assign-cores

num_cores = 3 in genesis only sets the relay-side budget; cores still have to be assigned to the para via the relay's Coretime::assign_core extrinsic before the parachain can actually use them — without it block production stays at ~12 s/block. The script sudo-batches one assign_core per core (same pattern as Bulletin's examples/assign_cores.js). After just start-chain, run just assign-cores once and the parachain settles at ~2 s blocks.

Plus a one-line drive-by in provider-discovery.ts: decode multiaddr via TextDecoder (papi 2.x returns raw bytes here, not a Binary).

No longer in this PR (superseded since the original version)

  • The DefaultCheckpointInterval/Grace re-expression as 10 * MINUTES / 2 * MINUTES — dropped in the Measure pallet timeouts in relay chain blocks, not parachain blocks #268 merge. Those constants are relay-denominated now (100/20 relay blocks ≈ the same 10 min / 2 min) and no longer rescale with parachain block time, which was the entire reason for that rework.
  • The examples/papi migration to polkadot-api 2.x — landed on dev independently and was absorbed via earlier merges.

Verified

The original 6s→2s bring-up was verified live against zombienet with just assign-cores applied: AuraApi::slot_duration() = 2000 ms, RelayParentOffsetApi::relay_parent_offset() = 1, para 4000 occupying relay cores 0/1/2, ~2–2.9 s/block sustained, and just demo / fs-demo-ci / s3-demo-ci passing end-to-end. Post-retarget onto #268: both runtimes compile, all paseo runtime tests (incl. XCM e2e and integrity_test) pass under the 2s constants; the live 2s zombienet run has not yet been repeated on the stacked branch.

ilchu added 2 commits June 4, 2026 18:40
Switch both runtimes from 6s blocks to 2s blocks with 3 cores assigned
per parachain, mirroring polkadot-bulletin-chain PRs #417 (slot-based
authoring) and #231 (2s + 3 cores).

Closes #113.

Runtime changes (both web3-storage and Paseo):
- MILLISECS_PER_BLOCK: 6000 -> 2000
- BLOCK_PROCESSING_VELOCITY: 1 -> 3 (3 parachain blocks per relay slot)
- UNINCLUDED_SEGMENT_CAPACITY: 3 -> (3 + RELAY_PARENT_OFFSET) * VELOCITY = 12
- New RELAY_PARENT_OFFSET = 1, wired into cumulus_pallet_parachain_system::Config
  and the RelayParentOffsetApi runtime impl (slot-based authoring requirement)
- RELAY_CHAIN_SLOT_DURATION_MILLIS stays 6000 (relay slot is independent)
- spec_version: 1 -> 2

Storage-pallet checkpoint cadence is now wall-clock-coupled so it
survives block-time changes: DefaultCheckpointInterval = 10 * MINUTES,
DefaultCheckpointGrace = 2 * MINUTES (resolves to the prior 100/20
blocks at 6s and 300/60 at 2s — same ~10 min cadence either way).
All other timeouts in storage.rs (ChallengeTimeout, SettlementTimeout,
RequestTimeout, DeregisterAnnouncementPeriod) auto-rescale via HOURS.

Zombienet topology (zombienet.toml + zombienet/storage-paseo-local.toml):
- 6 relay validators (alice, bob, charlie, dave, eve, ferdie) to support
  3-core scheduling
- Relay genesis patched with scheduler_params.num_cores = 3 and
  async_backing_params (allowed_ancestry_len = 6, max_candidate_depth = 6)
- Parachain num_cores = 3; second collator (bob) added
- Both collators run with --authoring=slot-based

New examples/papi/assign-cores.js + `just assign-cores` recipe:
zombienet's `num_cores=3` only sets the relay-side budget; actual
assignment of cores to a para needs an explicit Coretime::assign_core
extrinsic. The script sudo-batches one assign_core per core (mirrors
polkadot-bulletin-chain's examples/assign_cores.js). Use after
`just start-chain` to realize full 3-core throughput locally.

Verified:
- cargo fmt / check / clippy --all-features clean on both runtimes
- RPC reports AuraApi::slot_duration() = 2000 ms and
  RelayParentOffsetApi::relay_parent_offset() = 1
- Relay availability_cores shows para 4000 occupying cores 0/1/2 after
  assign-cores
- ~2.5-2.85s avg parachain block production (sampled over 60-91s windows)
- fs-demo-ci (Rust subxt L1) passes end-to-end

Known limitation:
The PAPI demos under examples/papi/ are still on polkadot-api 1.23 while
the rest of the repo is on 2.x. At 2s blocks PAPI 1.23's chainHead pin
window evicts the runtime-context block before `getValue()` can resolve,
producing BlockNotPinnedError. Migrating examples/papi/ to PAPI 2.x is
substantive (event payload reshape, H256 -> SizedHex<N>, signature enum
form, etc.) and tracked as follow-up; just demo will be red on dev
until that lands.
Brings examples/papi in line with the rest of the repo (the UIs are
already on polkadot-api@^2.1.0 + @polkadot-api/cli@^0.20.4) and gets the
PAPI demo working under 2-second blocks.

The previous full-flow.js failure modes on this branch (BlockNotPinnedError
on the first storage query after a tx) turned out to be two separate
issues, both of which are addressed here:

- Event watching destabilises chainHead. On PAPI 2.x, a global
  `api.event.X.Y.watch().subscribe()` interferes with the pinned-block
  set; subsequent `api.query.X.Y.getValue(...)` then throws
  `BlockNotPinnedError ... (getRuntimeCtx)`. `watchDefendedEvents` is
  removed — `respondToChallenge` already returns the ChallengeDefended
  event extracted from its own extrinsic events, so the demo counts
  those instead.
- finalizedBlock$ is now a BehaviorSubject. `waitForNextBlock` /
  `waitForBlock` declared `const sub = papi.finalizedBlock$.subscribe(...)`
  and referenced `sub` from inside the callback. On 2.x the BehaviorSubject
  fires synchronously on subscribe, before the `const` is assigned — TDZ
  ReferenceError. Switched to a `let sub; sub = papi.finalizedBlock$.subscribe(...)`
  pattern guarded by a `resolved` flag.

Type/shape adjustments to match the 2.x generated descriptors
(`SizedHex<N>` replaces `Binary` for `H256` / `[u8; N]` fields,
anonymous enums take `{ type, value }` object form):

- `mmr_root`, `data_root`, peaks, siblings: drop
  `Binary.fromBytes(hexToBytes(...))`, pass the raw hex string straight
  through.
- `provider_signature`: drop `Enum("Sr25519", Binary.fromBytes(...))`,
  use `{ type: "Sr25519", value: hexSignature }` (MultiSignature is an
  AnonymousEnum in the descriptors).
- `respond_to_challenge` response: same — `{ type: "Proof", value: proof }`.
- `chunk_data`: `Uint8Array` field, pass raw `Uint8Array` instead of
  `Binary.fromBytes(...)` (the descriptor type check now rejects Binary
  where Uint8Array is required).

Other plumbing for the version bump:

- `requireOneEvent` now returns `matched[0].payload` — 2.x wraps filtered
  events as `{ original, payload }`.
- Import path `polkadot-api/ws-provider` → `polkadot-api/ws` (and in
  assign-cores.js).
- `papi:generate` script: trailing `&& papi` is a no-op on 2.x; replace
  with `&& papi generate`.

Verified end-to-end against a running zombienet (chain producing at
~2.5–2.8 s/block after `just assign-cores`):

- `just demo`: all 8 steps green, "PASSED: Provider received payment!"
- `just drain-tx-pool-then fs-demo-ci`: passes
- `just drain-tx-pool-then s3-demo-ci`: passes
@ilchu ilchu self-assigned this Jun 4, 2026
@bkontur

bkontur commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

let's wait with this untill we resolve #129 and #130

ilchu and others added 5 commits June 16, 2026 14:50
The 'Merge branch dev into ic/2s-blocks-3-cores' conflict resolution kept
both the feature branch's @polkadot-api/cli bump (^0.20.4) and dev's new
c8 dependency, but dropped the comma between them, producing invalid JSON.
# Conflicts:
#	runtimes/web3-storage-paseo/src/storage.rs
…c_version, document UNINCLUDED_SEGMENT_CAPACITY

- examples/papi/common.js: drop undeclared `resolved` reference (ReferenceError)
  and redundant unsubscribe in waitForNextBlock/waitForBlock
- paseo runtime: bump spec_version 4_001 -> 4_002 for the consensus change
- runtimes: replace magic 3 in UNINCLUDED_SEGMENT_CAPACITY with a named
  RELAY_SLOTS_OF_CAPACITY constant + derivation comment
@bkontur

bkontur commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

@ilchu please, you can continue, after W3S presentation (today 15:00 CET), we can go with this to PreviewNet for sure

ilchu added 14 commits June 18, 2026 20:52
The migration check builds one post-upgrade block on a snapshot of the live chain and mocks its Aura slot as snapshot_timestamp / blocktime. previewnet paseo runs at slot_duration 6000, so the stored Aura slot is timestamp/6000 and pallet-aura requires the new slot not to decrease, i.e. blocktime <= 6000. The 24000 value made the mocked slot ~4x smaller and tripped "Slot must not decrease". 2000 matches the new SLOT_DURATION and stays valid once previewnet itself moves to 2s blocks.
…line

The 2.x migration bumped polkadot-api and @polkadot-api/cli but left @polkadot-api/substrate-bindings at ^0.16.5, a 1.x-era codec line. With no committed lockfile, that floor hoists into the generated descriptors, so their checksums are computed on a different codec than polkadot-api@2.x uses at runtime — failing every StorageProvider call with "Incompatible runtime entry". Bump to ^0.20.3 (what polkadot-api@2.1.6 and the user-interfaces workspace already use) and pin polkadot-api ^2.1.6.
The 2.x migration (0894b92) bumped polkadot-api/cli but left the Binary
value-class API in place; substrate-bindings@0.16.5 still shipped
Binary.fromBytes so it ran locally, masking the break. With bindings on
the 2.x line (0.20.x) that API is gone — Vec<u8>/[u8;N] fields are plain
Uint8Array, AccountId is SS58String, and H160 is a hex string.

- write sites: drop the Binary.fromBytes(x) wrapper; pass the Uint8Array
  directly (including the inline (await import(...)).Binary.fromBytes form
  in the e2e specs)
- read sites: drop .asBytes()/.asText() on Uint8Array fields; decode
  multiaddr via TextDecoder (the defensive `x.asBytes ? ... : x` guards
  were already 2.x-safe)
- revive: dest is SizedHex<20> (hex string, as returned by
  instantiated.contract); code/data are Uint8Array
- drop now-unused Binary imports

Verified against live previewnet: Providers reads yield Uint8Array
multiaddr/public_key; update_provider_multiaddr encodes from a raw
Uint8Array.
…Array

The previous commit converted all byte fields to raw Uint8Array, but that
is only correct for variable Vec<u8> (multiaddr, name, key, content_type,
code, data). Fixed-size [u8; N] fields are SizedHex<N> in polkadot-api 2.x
— i.e. 0x-prefixed hex strings — and passing a Uint8Array there fails the
arg-shape check as "Incompatible runtime entry":

- establish_storage_agreement sig (MultiSignature, [u8; 64]) — hex
- put_object_metadata cid (blake2-256, [u8; 32]) — hex; the read-side
  comparison now compares the SizedHex strings directly

provider-supplied fixed fields (mmr_root, provider_signature, mmr proof
hashes) already arrive as hex from the provider node, and the revive H160
dest is the hex string returned by instantiated.contract — all unchanged.

Verified against live previewnet with the CI dependency set: establish
(hex sig) and put_object_metadata (hex cid) encode COMPATIBLE; the old
Uint8Array forms reproduce the "Incompatible runtime entry" error.
respondToChallenge returned the extracted ChallengeDefended payload, but
every consumer that uses the return (full-flow recordDefended, e2e/05)
reads result.events — which is undefined on a payload, so PAPI's
eventDescriptor.filter(undefined) threw "Cannot read properties of
undefined (reading 'filter')" right after the on-chain respond succeeded.

Return the full tx result instead; keep the requireOneEvent call only to
assert the challenge was defended (the sc-flow/sc-coverage callers ignore
the return and rely on that throw).
In polkadot-api 2.x the Revive contract address (Instantiated.contract,
ContractEmitted.contract) and the Revive.call `dest` are SizedHex<20> hex
strings, not byte arrays:

- deployContract: keep instantiated.contract as the hex string (toHex on a
  hex string coerces it to an array length and throws RangeError at deploy)
- h160ToSubstrate: accept hex or raw bytes for publicKey.set()
- decodeContractEmitted: feed viem hex data/topics without re-encoding the
  already-hex values
- sc-coverage: precompile addresses are SizedHex<20>, used directly as
  Revive.call dest (drop the now-unused hexToBytes)

Verified locally on an omni-node dev chain + provider: sc-flow, sc-coverage,
sc-team-drive, sc-token-gated and the L0 demo all pass end to end.
# Conflicts:
#	examples/papi/api.js
#	examples/papi/common.js
#	examples/papi/e2e/01-provider-registration.ts
#	examples/papi/e2e/03-s3-bucket-and-objects.ts
#	examples/papi/package.json
#	examples/papi/sc-api.js
#	examples/papi/sc-coverage.ts
After merging dev (PR #160 migrated examples/papi fully to TS),
assign-cores was the only remaining .js file in the repo: run via
bare `node` and excluded from `tsc --noEmit`. Convert it to .ts so it
matches the sibling papi scripts and gets typechecked.

- type the two implicit-any params (argv, seed)
- replace the push loop with Array.from to avoid an evolving any[]
  under strict
- run via `node --import tsx` in the justfile recipe

Keeps its self-contained makeSigner: it targets the relay chain via
the untyped getUnsafeApi(), so it deliberately avoids the parachain
SDK/descriptors. tsc --noEmit passes under strict.
Add a BlockNumberProvider associated type to pallet-storage-provider's
Config and route every block-number read in the pallet (and the drive/s3
registries, which reuse it via their Config supertrait) through it.
Production runtimes supply RelaychainDataProvider, so all timeouts,
expiries, checkpoint windows and nonce-recency checks are measured in
relay chain blocks (6s) and keep their wall-clock meaning when the
parachain block time changes (#113 / PR #131). Mocks supply System.

The challenge slash sweep can no longer probe the single key n in
on_finalize(n): relay numbers advance by a variable amount (including
zero) between consecutive parachain blocks. Replace it with a
drain-range sweep in on_initialize that tracks progress in a new
LastSweptChallengeBlock cursor, drains all deadline keys strictly below
the previous block's relay parent (provably unrespondable), and returns
the exact weight consumed instead of pre-reserving it. Two independent
bounds keep the block budget safe: a 32-key span cap on probing and a
MaxChallengesPerDeadline slash budget per block (the old single-key
worst case), with mid-key carry-over via the cursor.

Runtime timing constants now derive from a new relay_time module based
on RELAY_CHAIN_SLOT_DURATION_MILLIS instead of the parachain
MILLISECS_PER_BLOCK; values are numerically unchanged today.

Benchmarks advance both clocks via a set_block_number helper (the
provider implements set_block_number under runtime-benchmarks), and the
paseo runtime tests gain a set_clocks helper for the same reason.

Closes #233
The pallet now measures every duration against the relay chain clock,
so off-chain actors must snapshot
ParachainSystem.LastRelayChainBlockNumber instead of the parachain
height wherever a value meets an on-chain check.

Provider node: chain_state.current_block now tracks the relay block
anchored to each finalized parachain block (one extra storage read per
block), feeding /negotiate's valid_until and the checkpoint window
math; the fetch/decode is shared via a new
storage_client::substrate::fetch_last_relay_block_number helper.

JS SDK: new currentRelayBlock/waitForRelayBlock helpers in
@web3-storage/layer0. Demos and e2e suites switch their
CommitmentPayload nonce snapshots off System.Number (which would be
rejected on any network where relay numbers dwarf parachain heights)
and wait for agreement expiry / checkpoint windows on the relay clock.
full-flow's demo agreement duration grows 15 -> 40 relay blocks since
the relay clock keeps ticking while the parachain onboards.
Coverage-gated (tests/*_integration.rs) additions:
- /negotiate signs valid_until = relay current_block + RequestTimeout,
  asserted with a Paseo-scale relay number, and returns 503
  chain_state_not_ready when either the clock or the constant is
  still unknown (both refusal branches were untested).

Plus checkpoint-coordinator duty tests (coordinators target): windows
derive from the relay-scale block number and move only when the relay
clock crosses an interval boundary, never on a repeated relay parent
(velocity > 1) or an intra-window advance.

Provider coverage: 64.79% locally vs 64.59% base (the dip came from
the coordinator's relay-fetch lines, which need a live chain; the
gate ignores subxt_client.rs).
@ilchu
ilchu changed the base branch from dev to ic/relay-block-provider July 7, 2026 06:07
PR #131 now targets #268 (relay-block denomination) instead of dev.

Conflict resolutions:
- runtimes/*/storage.rs: took the relay-block-provider side wholesale.
  This PR's DefaultCheckpointInterval/Grace rework (10*MINUTES/2*MINUTES
  of parachain time) is obsolete — the constants are relay-denominated
  now (100/20 relay blocks = the same ~10 min / ~2 min wall-clock) and
  no longer rescale with parachain block time, which was the whole point
  of that rework.
- runtimes/*/lib.rs spec_version: both sides bumped independently from
  the merge base, so take the union: local runtime 2 vs 3 -> 4, paseo
  4_002 vs 4_003 -> 4_004, with lineage comments.
- Everything else (2s consensus constants, velocity 3,
  RelayParentOffset, zombienet topology, assign-cores) merged clean.
@ilchu
ilchu requested a review from bkontur July 7, 2026 07:37
ilchu added 4 commits July 13, 2026 18:11
#275 extracted the wire types into provider-negotiation and removed the
provider node's storage-client dependency, but two callers still reached
into storage_client::substrate::fetch_last_relay_block_number. Inline the
one-shot ParachainSystem::LastRelayChainBlockNumber query as a pub(crate)
helper in subxt_client so the node reads the relay clock over its own
subxt connection without depending on the client SDK.
The relay_time module's MINUTES/HOURS are name-twins of the parachain
time module's MINUTES/HOURS, so a call site like 48 * HOURS gave no hint
which clock it measured. Rename the relay-chain units to RC_MINUTES /
RC_HOURS in both runtimes (constants + storage.rs timeout params) so the
relay-chain denomination is explicit at every use. Values unchanged; the
parachain time::* units (Session Period/SessionLength) keep their names.
ilchu and others added 22 commits July 16, 2026 15:29
Rewrite the on_initialize sweep docs as bullets with the safety
formula (keys < previous relay parent are final), tighten
MAX_SWEEP_SPAN and LastSweptChallengeBlock, and fix stale on_finalize
references left over from the on_initialize move.

Addresses review comments on #268.
The benchmark still called the dropped on_finalize hook, which is now
the Hooks default no-op, so it measured nothing and under-charged the
slash sweep. Anchor the cursor and relay clock so on_initialize drains
exactly the loaded deadline key, and add a verify guard asserting the
drain actually happened.

Addresses review comment on #268.
The Automatic Slashing sequence still showed the old
on_finalize(block_number) single-key take. Reflect the relay-block
range drain tracked by LastSweptChallengeBlock.

Addresses review comment on #268.
A single deadline can hold up to MaxChallengesPerDeadline (1000)
challenges; slashing all of them in one block costs ~5 MB PoV — the
whole block budget. Introduce MAX_SWEEP_SLASH_BUDGET (100) and cap the
per-block budget at min(MaxChallengesPerDeadline, MAX_SWEEP_SLASH_BUDGET),
letting a full deadline drain over several blocks via the existing
carry-over cursor. Tighten the benchmark component to the effective
budget so weight regeneration stays within the sweep's real ceiling.

Addresses review comment on #268.
The field holds the relay-chain block anchored to the latest finalized
parachain block, not the parachain height. Rename it (and the local it
feeds in /negotiate) so the relay-clock semantics are self-evident,
resolving the ambiguity flagged in review. The separate get_current_block
trait methods are untouched.

Addresses review comment on #268.
Assert no unresolved challenge sits at or below LastSweptChallengeBlock:
the sweep drains everything up to its cursor and new challenges always
land above it, so a violation flags a stranded-unslashed challenge
(e.g. an un-migrated upgrade leaving keys below the anchor).

Addresses review comment on #268.
Conflict in pallet/src/lib.rs (both branches added a try_state hook):
- imports: kept our `One` and dev's cfg-gated `TryRuntimeError`.
- Hooks::try_state: took dev's delegating form (-> do_try_state), which
  supersedes our inline sweep-cursor check. The sweep-cursor invariant is
  re-homed into dev's impls/try_state.rs in the following commit.
The merge with dev adopted its Pallet::do_try_state() structure, which
superseded the inline try_state hook from the earlier commit. Move the
sweep-cursor invariant (no unresolved challenge at or below
LastSweptChallengeBlock) in as check_challenge_sweep_cursor (P1.5), with
a detection test matching the module's convention.
The ChainState.current_block -> current_relay_block rename missed the
tests/ integration files (the earlier clippy check omitted
--all-targets, so they were never compiled). Rename the field accesses
so `cargo clippy --all-targets` and the integration tests build again.
Per review: the provider should not reach into ParachainSystem's
LastRelayChainBlockNumber (a relay-specific storage item) to learn the
clock on-chain durations are measured against. Introduce a
block-notion-agnostic abstraction:

- Pallet: rename Pallet::current_block -> current_anchor_block (the sole
  place the BlockNumberProvider is read) and expose it via a new
  StorageProviderApi::current_anchor_block runtime API, implemented in
  both runtimes.
- Provider: replace the dynamic LastRelayChainBlockNumber storage read
  with a dynamic current_anchor_block runtime-API call, and rename
  ChainState.current_relay_block -> current_anchor_block. The provider no
  longer knows or cares whether the anchor is a relay, parachain, or
  future block number.
- Registries call sites updated to current_anchor_block.

Behavior-preserving: the runtime API resolves to the same value the
provider read before. Note: crates/storage-subxt static bindings are
regenerated from node metadata and do not yet include the new API; the
provider uses a dynamic call, so this is not a blocker.
dev's #288 bumped subxt 0.44 -> 0.50 across chain-facing crates, which
overlapped the two provider files this branch rewrote for the
current_anchor_block runtime API. Resolution:

- provider-node/src/subxt_client.rs (conflict): kept the anchor-block
  semantics but ported to subxt 0.50 — fetch via runtime_apis().call_raw
  ("StorageProviderApi_current_anchor_block") and decode the raw u32,
  which also avoids depending on the API being in the node metadata
  snapshot. current_anchor_block() now uses at_current_block().
- chain_state_coordinator.rs (silent auto-merge, 0.44 call left behind):
  fetch the anchor via block.at().runtime_apis(); hoisted the block
  handle so anchor + events share one at(). Added a state_call mock arm
  and updated the field-rename in the coordinator test.
- client/src/substrate.rs: took dev's 0.50 version. The branch's
  fetch_last_relay_block_number helper here was dead code (orphaned when
  the provider inlined its own copy in f636e25) and 0.44-only, so
  dropping it matches dev.

Full workspace compiles; pallet/provider/client/registry/paseo tests
green; clippy clean.
analyze_provider compared the parachain head against the relay-denominated
snapshot.checkpoint_block, so on live networks the age saturated to 0 and
every provider read as freshly checkpointed. Re-add the SDK-side anchor
helper (dropped with the subxt 0.50 signatures) as a runtime-API read and
measure the age on it, reusing the block-pinned handle for a consistent
snapshot.
Locals holding Pallet::current_anchor_block() were still named
current_block, reading as parachain height. Rename them (and the
checkpoint helpers' parameters) to anchor_block, matching the
negotiate handler's naming; fix the stale Pallet::current_block doc
link and the ChainStateNotReady messages along the way.
current_anchor_block tells clients where the pallet clock is but not how
fast it ticks, so the provider dashboard humanized anchor-denominated
durations (agreement duration, checkpoint interval/grace, min/max
duration) with Aura.SlotDuration — identical today, silently wrong once
the parachain block time changes. Add the paired runtime API (both
runtimes return RELAY_CHAIN_SLOT_DURATION_MILLIS) and read it in the
dashboard via the unsafe API (no descriptor regeneration; older runtimes
keep the 6s default), renaming blockTimeMs to anchorBlockTimeMs so
parachain block time can't be rewired in by accident.
The anchor_block_time_millis runtime API hardcoded
RELAY_CHAIN_SLOT_DURATION_MILLIS in each runtime's impl block, away from
the BlockNumberProvider wiring it describes. Make it a #[pallet::constant]
instead: the clock and its tick are now declared side by side in each
runtime's storage.rs, the runtime API delegates to the pallet like
current_anchor_block does, integrity_test rejects a zero value, and the
constant lands in metadata for free.
…arisons

The provider dashboard and s3-ui compared the parachain height against
anchor-denominated on-chain values, which silently passes on zombienet
and breaks on any network where relay numbers dwarf parachain heights:
agreement expiry and challenge status in chain-client.ts (whose local
blockNumber$ was additionally never fed, so both always read 0),
checkpoint-overdue detection, the deregister cooldown, earnings
progress, and the s3-ui challenge countdown.

Add an anchorBlock$ to each UI's chain state, refreshed per finalized
block from the current_anchor_block runtime API via the unsafe API (live
metadata, no descriptor regeneration; pre-anchor runtimes fall back to
the parachain height, which is their pallet clock), and point all six
comparisons at it.
The per-block anchor refresh is a fire-and-forget promise, so a result
resolving after disconnect() (or a network switch) could write a stale
anchor from the previous chain into the fresh state. Publish only if
the client the call was made on is still the current client.
Base automatically changed from ic/relay-block-provider to dev July 20, 2026 10:42
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.

Setup 2s blocks (3 cores) for testnet runtime and paseo one

2 participants