Skip to content

Support for 3 async cores#131

Open
ilchu wants to merge 22 commits into
ic/relay-block-providerfrom
ic/2s-blocks-3-cores
Open

Support for 3 async cores#131
ilchu wants to merge 22 commits into
ic/relay-block-providerfrom
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 12 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.
@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.
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