Skip to content

node-2.1.0-rc.1

Choose a tag to compare

@github-actions github-actions released this 15 Sep 15:53
f4e6be6

node 2.1.0-rc.1

Metadata

  • Release type: major
    • Semver basis: the last final release is node-1.0.1; 2.0.0 never shipped a final, so 2.1.0 is the first release of a new major line (release body; consistent with the node-2.1.0-beta.1 release body).
  • 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-ops for the string 2.1.0-rc.1 returned 0 hits, so no bundle pointer is sourced either.
  • Sister-line note: This release is on the 2.1.x line. Operators on the 1.0.x mainnet maintenance line should track node-1.0.2 (release body) instead. The fork-from baseline for this line is still the 1.0.3 release, which remains pending (node-2.1.0-beta.1 release 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/infra component 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.x line.
  • 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.1 release 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-in metadata/static/midnight_metadata.scale grows 137,010 to 144,075 bytes — while spec_version stays at 002_001_000 (commit f4e6be6, PR #2079). spec_version is deliberately not bumped: 002_001_000 has 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 the beta.1 runtime, so no consumer can be holding beta.1 metadata for it — see the Breaking-changes section.
  • The observed-cNIGHT-UTXO ordering rule is gated on spec_version at the parent block, with the threshold DATA_ORDERED_UTXOS_SPEC_VERSION set below this line's spec_version (PR #2123). A validator set must not run 2.1.0-beta.1 and 2.1.0-rc.1 binaries at the same time on a 2.x runtime: the binary rollout is a flag day, not a rolling upgrade (owner-confirmed 2026-09-16) — see the Breaking-changes section.
  • CNightObservationApi drops 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_version is unchanged from 2.1.0-beta.1, but the runtime WASM is not: SafeMode is 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 from 2.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 governance set_code. Enabling the C2M bridge additionally needs a governance action to set its MainChainScripts and data checkpoint; without it the bridge's inherent data provider stays Inert (release body).
  • Downtime / coordination: Coordinated, and for a chain already on 2.1.0-beta.1 binaries it is a flag day. Because spec_version does not move between the two releases and only rc.1 carries the new cNIGHT UTXO ordering (PR #2123), the two binaries must not author and verify blocks on the same 2.x chain 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 from 1.0.3 the beta.1 guidance still applies: roll every validator onto the new binary first, confirm the whole set is importing and finalizing, then enact set_code; Cardano observation is gated for the duration of the dust replay.

Artifacts

  • midnightntwrk/midnight-node:2.1.0-rc.1 — node image; index digest sha256:5a75bfa99643b49a9ba3f1a9f8e6820f3308f9fdae0902f21455ee43d4c2856e (linux/amd64 sha256:7a75759ab79ae51c9ab86e3bdf46561a14ccc32e75be7e616f15f6fa3c81d018, linux/arm64 sha256:07de2269127dfc2b4fccd533ebf0dad5ff72aaea384e1ef79f76282ca249dd91), with per-arch tags 2.1.0-rc.1-amd64 and 2.1.0-rc.1-arm64 (Docker Hub registry API)
  • midnightntwrk/midnight-node-toolkit:2.1.0-rc.1 — toolkit image; index digest sha256:d4f1414d772e373196161c50d47f58cc62b2a04d8b27d059d551d7e33a4d6600 (linux/amd64 sha256:03b915bd54d618fb44ec41c11919971cb8955d5a652ab29dad5236c8dba53d85, linux/arm64 sha256:89cc4a3e71b521fbac09944304b328b0ad5f4d28e7e165e19de4d56c88d8a1ba), with per-arch tags 2.1.0-rc.1-amd64 and 2.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.1 at commit f4e6be63108ddedefe74710805a6ba69f396bc62, tree 0bb0423db50d3b5405fa5367b0dfbe17daa3101b; sibling tags runtime-2.1.0-rc.1 and toolkit-2.1.0-rc.1 (release body)
  • Runtime WASM: not published — no *.wasm or srtool-digest.json asset 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_IMAGE still overrides the mock-authorities image; its default moves to 4d8d772 (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_SEED entries (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_*_SEED phrases at run time via --env-file or 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_genesis passes the spec_version of 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/Registration pair that left the indexer's cnight_registrations row 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.1 test chain before pointing it at an rc.1 one; do not rely on it re-fetching, because the spec_version it keys the cache on has not moved (PR #2111).
  • Do not pin production tooling against rc.1 metadata: 002_001_000 is 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.1 note: it points at docs/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.1 linked its metadata-refetching section here.
  • Known issues board: open bugs
  • Public schema: —
  • API documentation: TODO: no published reference for the midnight_contractState / SessionInfoApi RPC 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)