Why we updated the morning after
Nimiq published v2.3.0 at 22:58 UTC on 2 November 2026 and asked node operators to update as soon as possible: the release carries important security fixes. Like every 2.x release it is backwards compatible, so the timing was ours to choose. We chose the first quiet moment: about eight hours after the release and roughly six hours before the next epoch boundary.
The validator was elected for epoch 1,446 with slots 207 to 211, so it was actively producing blocks when we started. That is why the moment mattered: a swap that lands on one of your own slots costs that slot's reward.
Before touching anything: the rollback plan
Same rule as the 2.2.0 update: nothing gets stopped until everything needed to undo it exists.
- The new image, verified. The 2.3.0 image was pulled in advance and run once on its own to confirm it reports
nimiq-client 2.3.0. - The old image, tagged. The running 2.2.0 image was retagged
2.2.0-rollback, so a rollback is a one line container change. - The container spec, exported. The full
docker inspectof the working container was saved, so the replacement was created with exactly the same data volume, peer port and restart policy. - The data directory, snapshotted while stopped. New this time: the copy of the node's data (577 MB) was taken in the second between stopping the old node and starting the new one. A copy of a stopped node is guaranteed consistent; it cost about 1.3 seconds.
The node's watchdog and the restake bot were paused for the few seconds of the swap so neither would act on a node that was intentionally down, then restored from a single backup and checked line by line against it.
The swap, second by second
Everything below comes from the node's own logs and the chain, timestamps included.
| Time (UTC, 3 Nov) | Step | Evidence |
|---|---|---|
| 07:24:59 | 2.2.0 node stopped; last network block before the stop is #65,902,408 | docker stop, block timestamps |
| 07:25:00 | Data snapshot complete (577 MB) | snapshot step log |
| 07:25:02 | 2.3.0 container started on the same data volume | container StartedAt |
| 07:25:04 | Consensus established | RPC isConsensusEstablished answers true |
| 07:25:12 | Status line: head #65,902,420, 53 peers | node log line "Consensus: established" |
| 07:30:09 | First block produced on 2.3.0: micro block #65,902,720 | node log "Our turn", chain records us as producer |
| 07:33:19 | Restake bot's first full cycle after the swap: 129.14 NIM paid out | restake ledger |
Stop to consensus: about 5 seconds, against about 11 for the 2.2.0 update in September. The swap landed between our own blocks: the next block assigned to ImpactZero came five minutes later and was produced on time.
The checks that said it worked
A node that has restarted and a node that is producing are different claims. These checks separated them:
- Version: the running node reports
nimiq-client 2.3.0. - Checkpoint: zero checkpoint mismatches. Each release ships a new mainnet checkpoint (election block 64,627,200 for this one); a mismatch would stop the node from progressing.
- Validator record: active, not jailed, not retired, same balance and staker count as before the swap.
- Blocks produced: the next block assigned to us, micro block #65,902,720, was produced on time, and the chain lists ImpactZero as its producer.
- No penalties: zero skip blocks on the network around the swap and an empty punished set in every macro block from 07:23 to 07:30 UTC.
- Zero panics and zero error lines in the node log since the start, with 83 peers connected twenty minutes later.
What 2.3.0 actually changes
The release notes, translated into what each change means for a validator:
- A transaction cache that could skip a signature check. A shortcut in transaction verification could accept a cached transaction without checking its signature in one edge case. Fixed: every transaction is checked properly. This is the headline security fix.
- Less work for unauthenticated data. Election voting keys are now decompressed only after a block is authenticated, block bodies passed between peers have a size limit, and double vote proof signatures are decompressed only when needed. A misbehaving peer can no longer make a node do expensive work with data it has not proven.
- Tighter connection limits. Connection pool accounting was fixed and per subnet limits were tightened, which makes it harder to crowd out a node's peers from a single network range.
- Stricter proof checks. Proofs with duplicate positions and malformed trie keys are now rejected.
- More reliable state sync. Partial state stays consistent during sync and recent state changes are kept until the next macro block, so a syncing node is less likely to stumble.
- Validator blocks are published only after they are accepted. A validator no longer announces a block it failed to apply itself.
- A consensus edge case. Macro block voting can now finish on a late proposal that completes a set of votes it already holds, instead of losing a round.
- Stability and dependencies. A memory leak in address subscriptions and a race condition were fixed, cryptography libraries got constant time fixes, and dependencies were updated for published security advisories.
- Refreshed mainnet checkpoint at election block 64,627,200, so new and resyncing nodes start faster.
The ten minutes the restake bot waited
One thing happened around the update that was not the update. Before paying anyone, our restake bot checks the validator's recent incoming transactions so that a wrong send is never paid out as if it were a reward. That check reads from a public history service, and from just before the swap until 07:31 UTC that service was not answering. The bot did what it is built to do: it held its cycles rather than pay on unchecked data.
Held cycles lose nothing. Rewards stay in the validator's reward wallet, and the first good cycle at 07:33:19 UTC paid out the 129.14 NIM that had built up, split across every staker by the usual formula. Every cycle since then has run on schedule.
For other validator operators
The short version: pull and test the new image first, tag the running image for rollback, export the container spec, pause anything that would react to the node going down, then stop, snapshot, remove and recreate with the same spec. Watch for consensus and for your first produced block before you call it done, and check the punished set in the macro blocks after your swap. Pick the moment by your own slots and the epoch boundary rather than the clock: done that way, the update is a five minute job with a few seconds offline.