node-2.1.0-rc.1
node 2.1.0-rc.1
Metadata
- Release type: major
- Semver basis: the last final release is
node-1.0.1;2.0.0never shipped a final, so2.1.0is the first release of a new major line (release body; consistent with thenode-2.1.0-beta.1release body).
- Semver basis: the last final release is
- Date: 2026-09-15 (release body
publishedAt) - Ships in bundle: TODO: bundle line and bundle-RN link — not yet determined (owner-confirmed 2026-09-16). A code search of
midnightntwrk/midnight-network-opsfor the string2.1.0-rc.1returned 0 hits, so no bundle pointer is sourced either. - Sister-line note: This release is on the
2.1.xline. Operators on the1.0.xmainnet maintenance line should tracknode-1.0.2(release body) instead. The fork-from baseline for this line is still the1.0.3release, which remains pending (node-2.1.0-beta.1release body; owner-confirmed 2026-09-16). - Environment: devnet, qanet (owner-confirmed 2026-09-16) — no environment is named in the release body.
- Released artifact(s): eight artifacts publish at this tag — see Artifacts below.
- Component class:
core/infra - Upgrade scope:
binary + runtime - Reset required:
No - Governance action required:
Yes - Deployment information: the four fields above are answered in full under Deployment information below, as a
core/infracomponent must.
High-level summary
2.1.0-rc.1 is the release candidate that follows 2.1.0-beta.1: same spec_version (002_001_000) and transaction_version (4), with 29 further changes carried onto the release branch (fragment set at tag node-2.1.0-rc.1 vs node-2.1.0-beta.1). The one operators must read before upgrading is a consensus-affecting change to the order in which observed cNIGHT UTXOs reach the runtime (PR #2123): because spec_version does not move between beta.1 and rc.1, swapping binaries on a chain already running beta.1 is a flag day rather than a rolling upgrade (owner-confirmed 2026-09-16). Alongside it, a new SafeMode pallet keeps the chain producing blocks when a multi-block migration fails instead of freezing it permanently (PR #2079). Ledger pins move to 8.1.2 and 9.1.0.0-rc.5 (PR #2096, PR #2134), and the toolkit gets a large sync-and-replay performance and resilience pass (PR #1938).
Audience
These notes are written for:
- Full Node Operators (FNOs) running validators or archive/RPC nodes on a chain that will hard-fork from the
1.0.xline. - Shielded Technologies engineering and SRE, who own the enactment and the environment rollout.
- Midnight Foundation release coordination.
- External integrators whose clients, SDKs or indexers decode extrinsics, events or errors against the node's metadata.
- Developers using the toolkit to generate transactions or replay chain history.
Stake Pool Operators are out of scope for a release candidate on a non-public line.
Dependencies
- midnight-ledger 8.1.2 (crates.io) and midnight-ledger 9.1.0.0-rc.5 (per-crate git tags) — the node binary provides both via host calls (commit f4e6be6, PR #2096, PR #2134). 8.1.2 is a security patch that hardens low-level deserialization, so an 8.1.2 node accepts strictly less than an 8.1.1 one (PR #2096).
- The chain must already be running the 1.0.3 runtime — the fork-from baseline this line is built and tested against (
node-2.1.0-beta.1release body). Forking from 1.0.2 or earlier is not the documented path. - Ledger 7 remains fully removed on this line, so this node cannot decode or replay chain history predating the ledger 7 to 8 hardfork (PR #1999, announced in
node-2.1.0-beta.1).
Downstream impact (cascading effects).
- Runtime metadata changed relative to
2.1.0-beta.1— the checked-inmetadata/static/midnight_metadata.scalegrows 137,010 to 144,075 bytes — whilespec_versionstays at002_001_000(commit f4e6be6, PR #2079).spec_versionis deliberately not bumped:002_001_000has not been released onto any chain yet, so it is still being filled in rather than superseded (owner-confirmed 2026-09-16). No chain is running thebeta.1runtime, so no consumer can be holdingbeta.1metadata for it — see the Breaking-changes section. - The observed-cNIGHT-UTXO ordering rule is gated on
spec_versionat the parent block, with the thresholdDATA_ORDERED_UTXOS_SPEC_VERSIONset below this line'sspec_version(PR #2123). A validator set must not run2.1.0-beta.1and2.1.0-rc.1binaries at the same time on a2.xruntime: the binary rollout is a flag day, not a rolling upgrade (owner-confirmed 2026-09-16) — see the Breaking-changes section. CNightObservationApidrops back to v1 and its node-side version gate is removed, so every binary again over-fetches cNIGHT UTXOs at a flat 64x (commit dc8f0bd, release body).
Anything else about cross-component interop is answered by the bundle RN's matrix, not here.
Tested-with versions
These are build pins — not QA-verified — read from the workspace lockfile and pin files at the release tag, so they say what this release was compiled against rather than what it was tested against.
| Component | Tested-with version |
|---|---|
midnight-ledger (ledger 8) |
8.1.2 (commit f4e6be6) |
midnight-ledger (ledger 9) |
9.1.0.0-rc.5, per-crate tags (commit f4e6be6, PR #2134) |
polkadot-sdk |
polkadot-stable2606 (commit f4e6be6) |
partner-chains |
1.8.1 (commit f4e6be6) |
compactc |
0.33.0-rc.1 (commit f4e6be6) |
subxt |
0.50.0 and 0.44.3 (commit f4e6be6) |
parity-db |
0.5.4 (commit f4e6be6) |
| Rust toolchain | 1.95 (commit f4e6be6) |
midnight-indexer |
TODO: not yet available (owner-confirmed 2026-09-16) |
Deployment information
- Upgrade scope:
binary + runtime— a new binary and image, plus a coordinated on-chain runtime upgrade.spec_versionis unchanged from2.1.0-beta.1, but the runtime WASM is not:SafeModeis added at pallet index 20 and metadata is rebuilt (commit f4e6be6, PR #2079). - Reset required:
No— no state wipe, re-sync or re-index is called for by any change in this delta. One caveat inherited from2.1.0-beta.1: a toolkit block cache holding genuinely ledger-7 blocks is rejected with a "delete the cache and re-fetch" error. Separately, a wallet-state cache saved by an older toolkit under ledger 8 is now usable rather than silently discarded (PR #1938). - Governance action required:
Yes— the runtime is swapped by a governanceset_code. Enabling the C2M bridge additionally needs a governance action to set itsMainChainScriptsand data checkpoint; without it the bridge's inherent data provider staysInert(release body). - Downtime / coordination: Coordinated, and for a chain already on
2.1.0-beta.1binaries it is a flag day. Becausespec_versiondoes not move between the two releases and onlyrc.1carries the new cNIGHT UTXO ordering (PR #2123), the two binaries must not author and verify blocks on the same2.xchain concurrently (owner-confirmed 2026-09-16): stop the whole validator set, swap every binary, then restart. A rolling swap is not supported. For a fresh fork from1.0.3thebeta.1guidance still applies: roll every validator onto the new binary first, confirm the whole set is importing and finalizing, then enactset_code; Cardano observation is gated for the duration of the dust replay.
Artifacts
midnightntwrk/midnight-node:2.1.0-rc.1— node image; index digestsha256:5a75bfa99643b49a9ba3f1a9f8e6820f3308f9fdae0902f21455ee43d4c2856e(linux/amd64sha256:7a75759ab79ae51c9ab86e3bdf46561a14ccc32e75be7e616f15f6fa3c81d018, linux/arm64sha256:07de2269127dfc2b4fccd533ebf0dad5ff72aaea384e1ef79f76282ca249dd91), with per-arch tags2.1.0-rc.1-amd64and2.1.0-rc.1-arm64(Docker Hub registry API)midnightntwrk/midnight-node-toolkit:2.1.0-rc.1— toolkit image; index digestsha256:d4f1414d772e373196161c50d47f58cc62b2a04d8b27d059d551d7e33a4d6600(linux/amd64sha256:03b915bd54d618fb44ec41c11919971cb8955d5a652ab29dad5236c8dba53d85, linux/arm64sha256:89cc4a3e71b521fbac09944304b328b0ad5f4d28e7e165e19de4d56c88d8a1ba), with per-arch tags2.1.0-rc.1-amd64and2.1.0-rc.1-arm64(Docker Hub registry API)midnight-node-2.1.0-rc.1-linux-amd64.tar.gz— node binary;sha256:8e14d6ededa2b15082d2585bddc4d2361d66992d601a93c547dc572a126b7f56(release body)midnight-node-2.1.0-rc.1-linux-arm64.tar.gz— node binary;sha256:6916775131033e199364b6a5b0645281cbdcf3ddfa1d4b11c19353a6a049c975(release body)midnight-node-toolkit-2.1.0-rc.1-linux-amd64.tar.gz— toolkit binary;sha256:f49b7d2069a2a85e3d106feed23b67e5be94cd0994292d41a8811ec5b0a409b6(release body)midnight-node-toolkit-2.1.0-rc.1-linux-arm64.tar.gz— toolkit binary;sha256:c10c50ff0e74442d01266787b20498fb27e6cd43b6abf0b596af10bc864c219e(release body)SHA256SUMS-amd64— checksums for the amd64 tarballs;sha256:0bafe5fdad6a2b8c2780b38ebadb57a766d7e24604687d98de3db22abe5896d9(release body)SHA256SUMS-arm64— checksums for the arm64 tarballs;sha256:470a28f52bbc6ccf7c5575761373a6e99f64bcb4c9586480114c8807e5c69f54(release body)- Git tag
node-2.1.0-rc.1at commitf4e6be63108ddedefe74710805a6ba69f396bc62, tree0bb0423db50d3b5405fa5367b0dfbe17daa3101b; sibling tagsruntime-2.1.0-rc.1andtoolkit-2.1.0-rc.1(release body) - Runtime WASM: not published — no
*.wasmorsrtool-digest.jsonasset exists on this release (release body asset list). See Known issues.
What changed
The list below is the delta against node-2.1.0-beta.1, computed from the changes/ fragment set at each tag. Everything the 2.x line introduced relative to 1.0.x is in the 2.1.0-beta.1 and 2.0.0-alpha.1 notes and is not restated.
| Change | Upgrade type | Citation |
|---|---|---|
Order observed cNIGHT UTXOs by data variant, gated on spec_version |
Node upgrade | PR #2123 |
| Enter safe mode instead of freezing the chain on a failed multi-block migration | Runtime upgrade | PR #2079 |
| Per-pallet allow-listed system transaction executors | Runtime upgrade | PR #2080 |
| Bump ledger 8 to 8.1.2 and ledger 9 to 9.1.0.0-rc.5 | Node upgrade + Toolkit | PR #2096 |
| Pin ledger 9.1.0.0-rc.5 per crate instead of by the mutable workspace tag | Node upgrade + Toolkit | PR #2134 |
| Revert the 16x reduction in cNIGHT observation UTXO over-fetch | Node upgrade | commit dc8f0bd |
| Stop processing a Cardano transaction after a failed bridge transfer | Runtime upgrade | PR #1980 |
| Faster, resilient chain sync and wallet replay, with caching that works on mainnet | Toolkit | PR #1938 |
| Cache runtime metadata across blocks when fetching | Toolkit | PR #2111 |
--no-watch-progress sends now fail when the pool rejects the transaction |
Toolkit | PR #2138 |
Don't abort context replay on a well_formed failure the chain tolerated |
Toolkit | PR #2098 |
Fix update-ledger-parameters re-using one value across every block limit |
Toolkit | PR #2073 |
Log transaction rejection reasons via Display rather than Debug |
Node upgrade | PR #2105 |
Stop duplicating binaries and res/ in a second image layer |
Node upgrade + Toolkit | PR #2048 |
| Give each ledger version its own copy of the wrapper code, helpers and builders | Node upgrade + Toolkit | PR #2059, PR #2074, PR #2075 |
Split the can-panic part of midnight-ledger-helpers into its own crate |
Toolkit | PR #2106 |
Non-validator archive node, --num-validators, and a downstream-consumer contract in local-env |
Node upgrade | PR #2058, PR #2008, PR #1920 |
Fix GRANDPA finality stall when forking a >=2.0.0 network in local-env |
Node upgrade | PR #2008 |
Make fork-network full-upgrade mode resolve a reachable RPC endpoint, and enable qanet --from-genesis |
Node upgrade | PR #2032 |
Dependency and CI housekeeping: renovate major backlog, toml npm-audit override, canonical bot:* labels |
Toolkit | PR #2047, PR #2139, PR #2025 |
New features
Feature SafeMode pallet and failed-migration recovery
Description: A failed multi-block migration previously froze the chain permanently — the migration cursor became Stuck, only inherents were admitted into blocks, and can_set_code rejected runtime upgrades, leaving no on-chain recovery path on a standalone chain. This release adds pallet-safe-mode at pallet index 20 together with a custom FailedMigrationHandler that enters safe mode indefinitely and force-unstucks the cursor instead. The chain keeps producing blocks with user-facing calls filtered — only inherents and the governance allowlist pass — so governance can ship a fixed runtime and then force_exit safe mode. Until a migration actually fails, behaviour is unchanged. Requires a metadata rebuild. Runtime upgrade. (PR #2079)
Feature Non-validator archive node in the local-environment stack
Description: The local-env compose gains midnight-node-6, a non-validator full node running --state-pruning=archive --blocks-pruning=archive, so historical state and block bodies stay queryable over RPC for the whole chain history. It boots from the shared chain-spec, syncs from midnight-node-1 as bootnode, and exposes host ports 30338 (p2p), 9945 (RPC) and 9620 (Prometheus). It is labelled io.midnight.role: archive rather than validator, so verify-finality and the upgrade/consensus commands — which discover validators by that label — skip it. Node upgrade. (PR #2058)
Feature Local-environment contract for downstream repositories
Description: Downstream repositories — midnight-indexer first — can point their services at a local-environment fork through a stable docker network name and a per-run connection manifest, with no coupling to this repository's layout. Node upgrade. (PR #1920)
New features requiring configuration updates
Feature --num-validators for smaller mock authority sets in local-env forks
Required updates — local-env operators only:
- Flag:
--num-validators <count>on the local-env commands that bring up or roll a forked network (run,image-upgrade,full-upgrade, and the runtime-upgrade path) (PR #2008). - Environment variable:
MOCK_AUTHORITIES_IMAGEstill overrides the mock-authorities image; its default moves to4d8d772(PR #2008) — see Improvements. - SDK initialisation: no change (PR #2008).
Impact: The count must be a positive integer no larger than the network's mock.validatorServices list, and changing it requires --from-snapshot so the authority set and seeds can be regenerated — it is rejected on reuse runs and in --from-genesis mode. The active selection is persisted alongside the generated Compose override, so later restarts can omit the flag. The option is not supported by the standalone local-env stack, whose five-validator topology and keys are fixed (PR #2008).
Feature qanet --from-genesis validator seed references
Required updates — qanet local-env operators only:
- Config: nothing to change by hand. qanet's
SEED_PHRASE: $MIDNIGHT_NODE_<nn>_0_SEEDentries (node1 to node12) are now active in the network compose file, matching every other well-known network (PR #2032). - Environment variables: supply the
MIDNIGHT_NODE_*_SEEDphrases at run time via--env-fileor the environment, as before (PR #2032). - SDK initialisation: no change (PR #2032).
Impact: npm run run:qanet -- --from-genesis previously failed with "From-genesis mode needs at least one validator seed phrase, but none of the seed env vars are set ()" even when every seed was provided, because from-genesis discovers validators by reading each service's SEED_PHRASE reference and qanet's were all commented out. The node still consumes keys only from the generated *_SEED_FILE mounts, so nothing changes for snapshot or fork bring-up (PR #2032).
Improvements
Improvement: Faster, resilient chain sync and wallet replay in the toolkit
Description: The fetch pipeline moves to a multi-threaded tokio runtime — previously every fetch and compute worker shared one core. Fetch workers now reconnect with exponential backoff, up to 10 minutes per job, instead of failing the whole sync on a dropped WebSocket, and events and headers are fetched alongside blocks so the compute stage does no network I/O and no longer panics on RPC errors. The job pusher chases the finalized tip, so a sync ends at the current head, and a 10-second heartbeat reports rate, ETA and backlog. Replay verifies finalized history in proof-erased form — zero-knowledge proofs, signatures including unshielded-input signatures, and balancing are not re-verified — and instead compares the locally computed state root against the on-chain Midnight.StateKey after every block, aborting on any mismatch; that matches the toolkit's trust model, since it is a testing tool that trusts the node it talks to, and is about 3.5x faster through transaction-dense ranges. Ledger snapshots are now version-tagged and ledger-8 chains — mainnet today — are fully supported; the cache previously never saved below ledger 9, so every run replayed from genesis, and a warm rerun now takes seconds. --replay-checkpoint-interval checkpoints work on ledger-8 chains too. Toolkit. (PR #1938)
Improvement: Runtime metadata cached across blocks when fetching
Description: The subxt client config now caches runtime metadata per spec version for the lifetime of the client. Without a cache subxt downloaded the full metadata — about 130KB and two runtime calls — for every block it created an at-block handle for; against a remote node a chain fetch ran at roughly 30 blocks/s with the process idle. With the cache the same fetch runs at roughly 600 blocks/s with 8 fetch workers. The cache is per client, so a process talking to several chains never mixes their metadata. Toolkit. (PR #2111)
Improvement: Pool rejections surfaced in --no-watch-progress sends
Description: With --no-watch-progress the sender logged SENT as soon as submit_and_watch returned and dropped the subscription unread. A node whose transaction pool is full does not reject the submission as an RPC error — it accepts the subscription and delivers the rejection as the first stream event, Invalid or Dropped — so every such transaction was reported as sent and the loss was invisible to the caller. Under burst load on perfnet this hid up to 61% loss per run. The sender now reads the subscription up to the pool's first verdict, bounded at 5 seconds: Invalid, Dropped and Error fail the send and are logged with the existing INVALID_TRANSACTION, DROPPED_TRANSACTION and TRANSACTION_ERROR tags, while any other status returns immediately as before. If no status arrives within the bound, or the subscription ends first, the send still succeeds but logs NO_POOL_VERDICT with the reason, so unknown outcomes stay countable. Toolkit. (PR #2138)
Improvement: Bridge processing stops cleanly inside a partially handled Cardano transaction
Description: The bridge no longer advances its checkpoint when a transfer fails execution or serialization; the partner-chains bridge pallet retries it in the next block, and if it keeps failing the bridge stops processing rather than skipping the transfer. One Cardano transaction yields at most two transfers — a reserve transfer when it takes tokens out of the reserve, followed by an ICS transfer of the remaining tokens it locked in the illiquid circulating supply — so processing can now stop between them, which the new BridgeDataCheckpoint::TxReserveTransfer variant denotes. Because a checkpoint can point between the two transfers, the observability layer no longer has to return them together and cuts the transfers it returns at the configured limit wherever that falls; any MaxTransfersPerBlock of at least one guarantees progress, and the value only bounds throughput and inherent weight. Cardano observability also now understands a data checkpoint pointing at a Cardano transaction not yet fully reflected on the Midnight side. Runtime upgrade. Note that current validators (Cardano scripts) do not allow a transaction that ends up in two transfers (PR #1980).
Improvement: Per-pallet allow-listed system transaction executors
Description: MidnightSystemTransactionExecutor let any caller submit any SystemTransaction variant to the ledger, with no restriction beyond the root-origin extrinsic's own governance allow-list. pallet-cnight-observation and pallet-c2m-bridge now go through separate MidnightSystemTransactionCNightExecutor and MidnightSystemTransactionBridgeExecutor traits, each backed by a dedicated ledger host function — apply_cnight_system_transaction and apply_bridge_system_transaction — that accepts only the system transaction variants that pallet is allowed to construct. The governance extrinsic path was rebuilt the same way onto a new apply_governance_system_transaction host function. The previous apply_system_transaction and is_governance_allowed_system_tx host functions are kept, unused by new code, for backward compatibility with already-published runtime WASM and historical block replay. Runtime upgrade. (PR #2080)
Improvement: Ledger 8.1.2 and ledger 9.1.0.0-rc.5 pins
Description: Ledger 8 moves to 8.1.2, a security patch in which low-level deserialization is hardened so that non-canonical encodings and invariant-violating values are rejected — an 8.1.2 node therefore accepts strictly less than an 8.1.1 one. It also carries panic and overflow fixes in Dust parameters and seq, Zswap binding randomness, delta normalization, and contract-call cost accounting. Ledger 9 moves to 9.1.0.0-rc.5, required to move midnight-storage and midnight-storage-core to versions compatible with 8.1.2. A follow-up repins rc.5 per crate rather than by the workspace tag, because the ledger-9.1.0.0-rc.5 tag is mutable and was re-pointed after the first bump locked it, leaving Cargo.lock naming content the tag no longer did; same source, no node code changes. Two entries the published patchset omits are added explicitly, without which resolution fails: midnight-ledger-static (crates.io stops at 9.0.0) and midnight-zkir-v3 (publish = false, activated by test-utilities). Node upgrade + Toolkit. (PR #2096, PR #2134)
Improvement: cNIGHT observation over-fetch restored to 64x
Description: Reverts PR #1367, restoring the per-transaction UTXO over-fetch factor to a flat 64x in the mainchain-follower cNIGHT observation data source and in genesis construction. The 4x factor left too little headroom: the per-query row limit must exceed the UTXO count of the largest single Cardano transaction in the window, or the truncation step emits a partial transaction and validators disagree on the inherent payload. CNightObservationApi drops back to v1 and the node-side version gate is removed, so every binary again uses 64x unconditionally. The runtime acceptance envelope (UTXO_PER_TX_OVERESTIMATE = 64) is unchanged and was never lowered, so both the 4x and 64x fetch factors have always been accepted on-chain. Node upgrade. The revert was made for security reasons and reopens the sync-speed pressure the reduction was written to relieve — see Known issues.
Improvement: Context replay tolerates a well_formed failure the chain itself tolerated
Description: LedgerContext::apply_txs_collect_events, in both the ledger-8 and ledger-9 implementations behind update_from_block, aborted the whole block replay whenever a transaction's well_formed check returned Err — for example OutOfDustValidityWindow for a dust action whose ctime lands a couple of seconds past the including block's tblock. That made every toolkit workflow which replays a context — transaction generation, wallet inspection, faucets, test-data generators — unusable against a chain such as Preview carrying a transaction the chain itself had accepted: on-chain, pallet_midnight::send_mn_transaction hits the same check but only fails that one extrinsic's dispatch, without affecting block validity, so a well_formed failure alone is not evidence of an invalid block. The function now matches LedgerContextError::InvalidTransaction specifically and, on that variant, logs a warning and moves on to the next transaction; every other error variant still aborts. update_from_tx is untouched and keeps hard-failing, since a generator needs to know immediately if its own just-built transaction is doomed. Toolkit. (PR #2098)
Improvement: update-ledger-parameters no longer re-uses one value across every block limit
Description: The toolkit re-used the read_time block limit across all block-limit parameters, so update-ledger-parameters --block-limit-read-time 2000000000000 set read_time, compute_time, block_usage, bytes_written and bytes_churned all to that value. Toolkit. (PR #2073)
Improvement: Transaction rejection reasons logged via Display
Description: The mempool, pre-dispatch and apply sinks rendered a rejected transaction's TransactionInvalid with {:?}, printing the error's full payload — including any ledger StateValue embedded in it — instead of the one-line diagnostic its Display impl already provides; the PartialSuccess sink did the same for its whole per-segment result map. All of them now use Display, with the segment map rendered as [<segment>: <reason>, ...]. Log lines become shorter and readable, and the node-side InvalidError code returned to callers is unchanged, so nothing downstream of the log is affected. Node upgrade. (PR #2105)
Improvement: Node and toolkit images no longer duplicate their payload in a chown layer
Description: Both images copied their files as root and then ran chown -R appuser:appuser over the same paths. chown rewrites every file, so buildkit stored a second full copy of them in the chown layer — measured on the node image with a stand-in 133M binary, that layer was 210MB: the binary plus the whole 74M res/ tree, duplicated. The files are now copied with COPY --chown=appuser:appuser, and the remaining chown only covers directories created by RUN mkdir. The toolkit image was duplicating /toolkit-js as well. A side effect: chown -R … ./bin never reached the file it was aimed at, because /bin is a usr-merge symlink that chown -R does not traverse, so .envrc is now owned by appuser like the rest. Node upgrade + Toolkit. (PR #2048)
Improvement: GRANDPA finality no longer stalls when forking a >=2.0.0 network in local-env
Description: Forking a well-known network — devnet on the 2.0.0 runtime, for instance — brought the chain up and produced blocks via AURA, but GRANDPA never finalized: finalized stayed frozen at the snapshot height while best climbed indefinitely. The cause was the mock-authorities image the fork tooling defaulted to. The run --from-snapshot path used mock-authorities:0f347c5 and the fork-network CI workflow used 368fd98; both predate the GRANDPA client-side aux-storage rewrite. Those older images only rewrite the runtime Grandpa state and rotate the client voter via a ForcedChange digest, which deadlocks the substrate GRANDPA voter on the 2.0.0 runtime — the client voter's set-id freezes below the runtime's CurrentSetId and no prevotes are counted, not even a node's own. Both defaults move to 4d8d772, which plants the post-rotation voter set directly in client aux storage with the client set-id equal to the runtime's CurrentSetId, so finality resumes immediately after restart. MOCK_AUTHORITIES_IMAGE and the mock_authorities_tag workflow input still override the default. Node upgrade. (PR #2008)
Improvement: fork-network full-upgrade mode resolves a reachable RPC endpoint
Description: Follow-up to the runtime-mode fix for the loopback-to-published-port black-holing seen on some self-hosted runners. full mode previously ran the all-in-one full-upgrade:<network> command, which brings the fork up internally and always targets ws://127.0.0.1:9950; on a runner where that path is black-holed the governance submission hung until the job timeout. full mode now runs its two constituent phases directly so the same RPC endpoint discovery used by runtime mode can run in between — image-upgrade, then a probe preferring the published port but falling back to node1's docker bridge IP, then governance-runtime-upgrade --skip-run against whichever endpoint answered. The discovered endpoint is exported for the finality-wait and :code verification steps, and the decomposition is behaviour-identical to full-upgrade. Node upgrade. (PR #2032)
Improvement: Each ledger version owns its own copy of the wrapper code, helpers and builders
Description: Three directories were each compiled twice through a #[path] module-parameterization trick — once bound to the ledger-8 crates and once to the ledger-9 crates — with super:: or a local crate alias resolving differently in each instantiation. A reader of a file under common/ could not tell which ledger crate it referred to, and an edit meant for one version silently applied to both. Each version now has its own directory with absolute crate::ledger_N::… imports, so every file states the version it binds: ledger/src/{ledger_8,ledger_9}/, ledger/helpers/src/{ledger_8,ledger_9}/, and builders/{ledger_8,ledger_9}/ plus commands/fork/{ledger_8,ledger_9}/ in the toolkit. ledger/src/common/ — the SCALE types crossing the runtime/client interface, version-independent by design and compiled once — is renamed to ledger/src/boundary/, so no folder is called common any more, and grep -r '#\[path' --include='*.rs' now returns nothing. No behaviour change, since rustc already emitted both instantiations; all public paths are unchanged and runtime metadata is untouched. diff -r src/ledger_8 src/ledger_9 now shows exactly where the two versions diverge. A companion change splits the can-panic part of midnight-ledger-helpers into its own crate, so what consensus may use is separated from what is for the toolkit and tests. Node upgrade + Toolkit. (PR #2059, PR #2074, PR #2075, PR #2106)
Improvement: Dependency and CI housekeeping
Description: The renovate major-update backlog is cleared: actions/cache to v6.1.0 and azure/setup-kubectl to v5.1.0 in the CI base image, both pinned by commit; Docker Compose to v5.5.0, which needs the buildx plugin installed alongside it because compose v5 removed its internal buildkit builder and delegates build: to Docker Bake; the local-env contract-compiler base to node:24-slim; and eslint to v10, which drops @eslint/eslintrc and with it js-yaml. The paritytech/srtool "v1" offer was a false positive — published tags are <rust version>-<srtool version>, and splitting that across an ARG and a hard-coded prefix made Renovate read the rust half as the image version; the whole tag is now one ARG, so deterministic runtime builds keep resolving. Separately, toml is overridden to 4.3.0 to clear the npm audit gate, and dependabot now applies the canonical bot:github-actions or bot:dependencies labels per ecosystem. Toolkit. (PR #2047, PR #2139, PR #2025)
Deprecations
None in this delta. The StandardTrasactionInfo alias deprecated in 2.1.0-beta.1 is unchanged (release body).
Breaking changes
Breaking change Observed cNIGHT UTXO ordering is consensus-affecting
What changed: The Vec<ObservedUtxo> the node passes to pallet-cnight-observation's process_tokens inherent is now sorted by header.tx_position and then by the data variant — Deregistration, Registration, AssetSpend, AssetCreate — instead of by the legacy "created UTXOs before spent UTXOs" rule. The runtime processes that vector in order, and handle_create needs a valid registration — exactly one mapping for the owner — at the moment it runs. Under the legacy rule an AssetCreate in the same Cardano transaction as a registration change was processed before the mapping settled, so it silently produced no DUST generation event and no UtxoOwners entry, permanently, for that UTXO. Ordering all mapping mutations ahead of all asset creates removes both failing cases: registering and receiving cNIGHT in one transaction, and rotating a mapping — which passes through an ambiguous two-mapping state — in one transaction. tx_position deliberately stays the primary sort key, because one inherent's batch spans a whole observation window and only the real on-chain Cardano order keeps a UTXO's AssetCreate ahead of the AssetSpend consuming it. No runtime, runtime API or metadata change is needed (PR #2123).
What breaks: Quoting the source directly, because the exact blast radius matters: "Because the runtime's check_inherent compares the whole UTXO vector for equality, changing the order is consensus-breaking: a node using a different rule than the block author rejects the block, including on a full sync from genesis. The rule is therefore gated on DATA_ORDERED_UTXOS_SPEC_VERSION, resolved against the runtime in state at the parent block — the same runtime that executes the child block and that check_inherents is called against, so the authoring and verifying paths always agree. Runtimes below the threshold keep the legacy ordering." (PR #2123)
A 2.1.0-beta.1 binary and a 2.1.0-rc.1 binary must not validate the same 2.x chain concurrently (owner-confirmed 2026-09-16). The gate keeps the authoring and verifying paths of any one binary in agreement, but it does not reconcile the two releases with each other: this line's spec_version is 002_001_000 and is unchanged from beta.1, DATA_ORDERED_UTXOS_SPEC_VERSION is 2_000_000, and neither that constant nor sort_observed_utxos exists at the node-2.1.0-beta.1 tag (commit f4e6be6). A beta.1 binary therefore applies the legacy ordering to a runtime an rc.1 binary reads as new-ordering, and check_inherent rejects the resulting block either way round.
Required actions — for operators and indexer maintainers:
- Treat the binary swap as a flag day on any chain already running
2.1.0-beta.1: stop the whole validator set, replace every binary, then restart. Do not roll validators one at a time (owner-confirmed 2026-09-16). - Regenerate any genesis state built by an older binary if it must match this one:
generate_cnight_genesispasses thespec_versionof the runtime the binary was built with, since it replays the same pallet logic, so regenerated genesis states use the new ordering (PR #2123). - Indexer operators: mapping rotation no longer emits a transient
Deregistration/Registrationpair that left the indexer'scnight_registrationsrow without its backing UTXO reference (PR #2123).
Code example: — (the change is internal to inherent construction and exposes no client-facing API surface)
Breaking change Runtime metadata changed without a spec_version bump
What changed: SafeMode is added at pallet index 20 and the checked-in static metadata grows from 137,010 to 144,075 bytes, while spec_version stays at 002_001_000 and transaction_version at 4 (commit f4e6be6, PR #2079).
What breaks: Nothing on a deployed chain. 002_001_000 has not been released onto any chain, so the version is still being filled in rather than reused after enactment, and it is deliberately held (owner-confirmed 2026-09-16). The consequence is confined to pre-release testing: the runtime's metadata differs from 2.1.0-beta.1's while its advertised spec_version does not, and this release's own toolkit caches metadata "per spec version for the lifetime of the client" (PR #2111), so a client or toolkit process that talked to a beta.1 test chain and is then pointed at an rc.1 one must be restarted rather than trusted to re-fetch.
Required actions — testing environments only:
- Restart any client, indexer or toolkit process that resolved metadata against a
2.1.0-beta.1test chain before pointing it at anrc.1one; do not rely on it re-fetching, because thespec_versionit keys the cache on has not moved (PR #2111). - Do not pin production tooling against
rc.1metadata:002_001_000is not yet released and is still being filled in on this line (owner-confirmed 2026-09-16).
Code example: —
Known issues
Issue No runtime WASM asset on this release
Description: This release publishes no *.wasm and no srtool-digest.json asset (release body asset list). The deterministic srtool runtime build is blocked on the srtool image being pinned to an older Rust than the repo builds with; the tracking issue is still open (release body; node-2.1.0-beta.1 release body). A deterministic WASM is a requirement for a final release, so this still blocks 2.1.0 final.
Workaround (if any): Extract the blob from the node image at /artifacts-{amd64,arm64}/midnight_node_runtime.compact.compressed.wasm. Its hash is not independently reproducible, so for a governance action needing a verifiable artefact, wait for a release that publishes one (node-2.1.0-beta.1 release body).
Issue Sync from genesis remains slow
Description: The 16x reduction in cNIGHT observation UTXO over-fetch is reverted for security reasons, so the follower is back on the higher Postgres round-trip volume during block import (commit dc8f0bd). This is on top of the sliding-window cache revert already announced in 2.1.0-beta.1. The underlying sync-speed issue remains open (release body).
Workaround (if any): Restore from a database snapshot rather than syncing from genesis where that option exists (node-2.1.0-beta.1 release body). Note that the toolkit's own sync and replay path is substantially faster in this release and its wallet-state cache now works on ledger-8 chains (release body) — that is a separate code path from node block import.
Links and references
- QA test coverage / test evidence: TODO: no QA evidence link available yet (owner-confirmed 2026-09-16); no
docs/releases/2.1.0*test-evidence path exists at the tag either. - PRs: PR #2123, PR #2079, PR #2080, PR #2096, PR #2134, PR #1980, PR #1938, PR #2111, PR #2138, PR #2098, PR #2073, PR #2105, PR #2048, PR #2059, PR #2074, PR #2075, PR #2106, PR #2058, PR #2008, PR #1920, PR #2032, PR #2047, PR #2139, PR #2025
- Engineering docs: Ledger 9 and the C2M bridge, Storage separation config, 2.0.0-alpha.1 release notes (all three existence-checked at the release tag)
- Migration guides: TODO: the hardfork migration guide does not exist yet (owner-confirmed 2026-09-16). Do not reuse the link from the published
node-2.1.0-beta.1note: it points atdocs/release-notes/2.1.0-beta.1/migration-hardfork-1.0.x.md, which is absent both at this release tag and on the default branch, so that link does not resolve. - SDK docs: TODO: the same not-yet-written guide as above (owner-confirmed 2026-09-16);
beta.1linked its metadata-refetching section here. - Known issues board: open bugs
- Public schema: —
- API documentation: TODO: no published reference for the
midnight_contractState/SessionInfoApiRPC surface is available to link (owner-confirmed 2026-09-16). - GitHub release: node-2.1.0-rc.1
- Previous release on this line: node-2.1.0-beta.1
Fixed defect list
The following defects were fixed in node-2.1.0-rc.1.
| Defect number | Description |
|---|---|
| #2121 | A DUST address change left cNIGHT untracked: a valid mapping with no utxoOwners entry and no DUST generation, permanently, for the affected UTXO (PR #2123) |
| #1937 | Toolkit mainnet sync and replay was slow and opaque, and the wallet-state cache never persisted on ledger-8 chains (PR #1938) |
| #2070 | Toolkit could not sync against Preview: context replay aborted on a transaction the chain itself had accepted (PR #2098) |
| #1768 | Shared common modules were compiled twice under different ledger bindings, so an edit meant for one ledger version silently applied to both (PR #2059, PR #2074, PR #2075) |
| #2114 | The ledger-9.1.0.0-rc.5 workspace tag is mutable and was re-pointed, leaving Cargo.lock naming content the tag no longer did (PR #2134) |
| #2024 | Dependabot and renovate PRs did not carry the canonical bot:* labels (PR #2025) |
| #1979 | Bridge advanced its data checkpoint past a transfer that failed execution or serialization (PR #1980) |
| — | --no-watch-progress sends reported SENT for transactions the pool had rejected, hiding up to 61% loss per run under burst load on perfnet (PR #2138) |
| — | update-ledger-parameters applied the read_time value to every block limit (PR #2073) |
| — | npm run run:qanet -- --from-genesis always failed with an empty seed-variable list (PR #2032) |
| — | fork-network full-upgrade mode hung until the job timeout on runners where the host loopback to published-port path is black-holed (PR #2032) |
| — | Forking a >=2.0.0 network in local-env produced blocks but never finalized (PR #2008) |
| — | Node and toolkit images stored a second full copy of their binaries and res/ in a chown layer (PR #2048) |
| — | toml@^3.0.0, pulled in by @effect/cli, carried GHSA-82x6-q7mm-w9cf (uncontrolled recursion) and GHSA-v5mp-jgw5-2x6j (prototype pollution), failing the npm audit gate on every PR (PR #2139) |