Skip to content

feat(runtime): reduce block time from 12s to 6s#4042

Merged
Kailai-Wang merged 6 commits into
devfrom
feat/block-time-6s
Jun 22, 2026
Merged

feat(runtime): reduce block time from 12s to 6s#4042
Kailai-Wang merged 6 commits into
devfrom
feat/block-time-6s

Conversation

@Kailai-Wang

Copy link
Copy Markdown
Collaborator

Summary

Reduce the parachain block time from 12s to 6s, producing one parachain block per 6s relay slot. Async-backing infrastructure (FixedVelocityConsensusHook, lookahead collator, 6s relay slot) is already in place, so this is a runtime-level change: a constant adjustment plus one-shot migrations to rescale stored state that encodes absolute block numbers or per-block rates.

BLOCK_PROCESSING_VELOCITY stays 1 and AllowMultipleBlocksPerSlot stays false — multi-block-per-slot / elastic scaling is intentionally left for a later step.

Changes

Constants (heima + paseo)

  • primitives: MILLISECS_PER_BLOCK 12000 → 6000. MINUTES/HOURS/DAYS/YEARS derive from it, so all time-denominated period constants (governance, treasury, staking round length default, identity, ...) auto-double and keep the same wall-clock duration.
  • runtime/common: UNINCLUDED_SEGMENT_CAPACITY 1 → 2 (pipeline headroom).
  • ProposalLifetime: hard-coded 504007 * DAYS. It does not auto-rescale; left unchanged it would have silently halved the bridge proposal voting window to ~3.5 days.
  • spec_version 9261 → 9270.

One-shot migrations (runtime/{heima,paseo}/src/migration/block_time_6s/, remove in the release after this one)

Only stored state that does not auto-rescale is touched:

  • vestingSteppedMigration (multi-block; ~2770 schedules on mainnet). Keeps already-vested funds vested and drains the remainder from the upgrade block at half the old rate, so each schedule completes at the same wall-clock time. per_block clamped to ≥1; fully-vested schedules dropped.
  • parachain-staking — reset Round.length to DefaultBlocksPerRound (the new doubled value), rebase Round.first.
  • score-staking — reset RoundConfig.interval to the new default (keeping the chain's existing stake coefficients), rebase Round.start_block.
  • scheduler — rebase anonymous, non-periodic Agenda tasks to the same wall-clock firing time (the only kind present on-chain).

OnePassRescale carries a once-only storage guard so re-execution is a no-op (the rebases are not naturally idempotent). The vesting MBM is run-once via pallet_migrations.

Scope confirmed against live state (RPC)

Scanned mainnet + Paseo before writing migrations. Surfaces needing rescale: vesting, parachain-staking round, score-staking, one anonymous scheduler task. Empty/not-applicable on both chains (no migration code): treasury spends, bounties (all Proposed), democracy ongoing referenda & conviction locks, chain-bridge in-flight votes, proxy announcements, identity pending usernames.

Collator impact

None required. The node reads slot_duration from the runtime (sc_consensus_aura::slot_duration) and already uses the lookahead collator, so existing collators pick up 6s automatically after the runtime upgrade — no flag or binary change needed.

Verification

  • cargo check — heima + paseo × (prod + try-runtime), all clean.
  • cargo fmt --check, cargo test -p heima-runtime (incl. 4 new vesting rescale unit tests).
  • try-runtime on-runtime-upgrade --checks=all against live mainnet and Paseo state — both pass: idempotency re-run produces an identical storage root, the vesting MBM completes, all pallet try-state checks pass, full runtime state decodes, weight is 0.44% of max. (The first try-runtime run caught a non-idempotent rebase, fixed by the once-only guard.)

Rollout

Recommended: merge → confirm CI try-runtime job green → Paseo runtime upgrade first (observe stable 6s, unchanged real-world period durations, vesting curve, staking round) → mainnet setCode → after MBM completes and is confirmed, remove the one-shot migrations next release.

Halve the parachain block time by setting MILLISECS_PER_BLOCK to 6000, so
one parachain block is produced per 6s relay slot (BLOCK_PROCESSING_VELOCITY
stays 1; multi-block-per-slot / elastic scaling is left for a later step).

Constant changes (heima + paseo):
- primitives: MILLISECS_PER_BLOCK 12000 -> 6000 (DAYS/HOURS/... auto-double,
  so time-denominated period constants keep the same wall-clock duration)
- runtime/common: UNINCLUDED_SEGMENT_CAPACITY 1 -> 2 (pipeline headroom)
- ProposalLifetime: hard-coded 50400 -> 7 * DAYS (would otherwise silently
  halve the bridge proposal voting window to ~3.5 days)
- spec_version 9261 -> 9270

One-shot migrations (migration/block_time_6s/, remove next release) that
rescale stored state encoding absolute block numbers or per-block rates,
which do not auto-rescale with the constant change:
- vesting: SteppedMigration (multi-block; ~2770 schedules on mainnet) that
  keeps already-vested funds vested and drains the remainder at half rate
  from the upgrade block, preserving each schedule's wall-clock completion
- parachain-staking: reset Round.length to DefaultBlocksPerRound, rebase first
- score-staking: reset RoundConfig.interval (keep stake coefficients), rebase
  Round.start_block
- scheduler: rebase anonymous non-periodic Agenda tasks to the same wall-clock
  firing time (the only kind present on-chain)

Verified via cargo check (heima/paseo x prod/try-runtime), cargo fmt --check,
and cargo test -p heima-runtime (incl. new vesting rescale unit tests).
try-runtime on-runtime-upgrade runs each migration twice to verify
idempotency; the scheduler/staking rebases in OnePassRescale changed the
storage root on the second run (a rebased scheduler task would be pushed out
again). Guard the whole migration with a once-only storage marker
(twox_128("BlockTime6s") ++ twox_128("OnePassRescaleDone")) so re-execution
is a no-op. The vesting MBM is already run-once via pallet_migrations.

Verified with try-runtime on-runtime-upgrade --checks=all against live
heima mainnet and Paseo state: storage root now identical across the
idempotency re-run, MBM completes, and all pallet try-state checks pass.
@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
heima-aa-demo-app Ignored Ignored Jun 22, 2026 8:11pm

Request Review

The `try-runtime.yml` workflow used `paritytech/try-runtime-gha`, which runs
`cargo` from the repo root and fails here with "could not find Cargo.toml"
because the workspace lives under `parachain/`. The action has no
working-directory input. Replace it with a self-contained build+run that
`cd parachain` first, mirroring the working try-runtime job in
`check-runtime-upgrade.yml`: install the rust toolchain, download the
try-runtime CLI, build the runtime with --features try-runtime, then run
`on-runtime-upgrade --checks=all --blocktime 6000` against the live URIs in
.github/runtime.json.

Also add a path-filtered `pull_request` trigger so runtime/primitives/pallets
changes get try-runtime coverage automatically, not only on manual dispatch.

check-runtime-upgrade.yml: bump TRYRUNTIME_VERSION 0.8.0 -> 0.10.1 and pass
the block time via a BLOCKTIME=6000 env var instead of the hard-coded 12000
(blocks are 6s after the spec 9270 upgrade).
Drop the pull_request trigger added in the previous commit. try-runtime
compiles both runtimes from scratch and scrapes full live chain state
(~10+ min per runtime), which is too heavy to run on every runtime PR.
Keep workflow_dispatch so it can be run on demand on a feature branch when
a migration warrants it. The parachain/ workspace-layout fix is retained.
Both try-runtime jobs passed a hard-coded --blocktime (6000), which is wrong
whenever the workflow runs against a different runtime: the old 12s runtime,
or a future elastic-scaling target with a shorter block time.

Read it dynamically instead. Aura's slot duration is twice
`Timestamp::MinimumPeriod`, so subwasm extracts that constant (a little-endian
u64, in ms) from the built wasm's metadata and the job uses 2 * MinimumPeriod
as the block time. This tracks whatever runtime is being tested with no manual
update. Fails loudly if the constant can't be parsed.

try-runtime.yml now installs subwasm for this; check-runtime-upgrade.yml
already had it, so its BLOCKTIME env is dropped.
Add integration tests for OnePassRescale that schedule a real anonymous,
non-periodic balances.burn (the same shape as the live heima on-chain burn:
a Root-scheduled batchAll of dispatchAs + balances.burn) and run the actual
migration:

- scheduled_burn_keeps_wall_clock_expiry_after_block_time_halving: a task
  100_000 blocks out is rebased to now + 2*(when-now), nothing is dropped or
  duplicated, and old (blocks*12s) == new (blocks*6s) wall-clock fire time.
- migration_is_idempotent: a second on_runtime_upgrade run does not rebase
  again (the once-only guard holds).

Needs sp-io as a dev-dependency for TestExternalities (matches runtime-common).
@Kailai-Wang
Kailai-Wang enabled auto-merge (squash) June 22, 2026 20:21
@Kailai-Wang
Kailai-Wang disabled auto-merge June 22, 2026 21:19
@Kailai-Wang
Kailai-Wang merged commit 63e99b5 into dev Jun 22, 2026
19 of 25 checks passed
@Kailai-Wang
Kailai-Wang deleted the feat/block-time-6s branch June 22, 2026 21:19
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.

2 participants