Protocol Versioning For Nooschain Developers
Protocol versioning keeps NOOSChain replayable while the project evolves. A block stores protocol_version, and that value selects the deterministic execution rules used for that block.
The invariant is simple and strict:
Historical replay must execute a block with the protocol version recorded on that block, not with the newest handlers in the current binary.
Read this page before changing transaction handlers, validation, signer resolution, replay, sync, snapshots, state-root interaction, or smart-contract runtime requirements.
For the conceptual introduction, see General Protocol Versioning.
Mental Model
A protocol version is a versioned dispatch table for deterministic block execution. It answers:
- which transaction handlers are available;
- how transaction payloads are validated;
- how actors are resolved from signers;
- which deterministic policies are applied;
- how success and failure are committed;
- which runtime requirements are used when contract execution is part of block execution.
The version is not a node setting. It is block metadata, part of block identity, and part of replay.
Unknown versions fail closed. A node that does not know how to execute protocol 2 must reject or fail replay for protocol 2 blocks instead of pretending they are protocol 1 blocks.
Does This Need A New Protocol Version?
Use this table before changing internals.
| Change | New protocol version? | Reason |
|---|---|---|
| Change signer or actor resolution | Yes | The same transaction could be authorized as a different actor. |
| Change nonce behavior | Yes | Replay could accept, reject, or order effects differently. |
| Change permission checks or implication rules | Yes | The same payload could succeed or fail differently. |
| Change bucket creation, bucket keys, or encrypted-record semantics | Yes | Committed state and state roots can change. |
| Add, remove, or change a deterministic transaction handler | Yes | Handler dispatch is protocol behavior. |
| Change deterministic failure behavior | Yes | Failed transactions are still part of replay semantics. |
| Change smart-contract runtime requirements used during block execution | Yes | Contract calls can produce different results. |
| Change registry, provenance, or deactivation policy that affects committed execution | Yes | Package and contract validity can change. |
| Refactor handler code with byte-for-byte equivalent effects | Usually no | Prove equivalence with tests. |
| Optimize performance without changing validation, effects, hashes, or failures | Usually no | Execution result is unchanged. |
| Add observability-only fields | Usually no | Operator visibility is not block semantics. |
| Change logs, CLI formatting, Admin GUI, or docs | No | No deterministic block output changes. |
| Change local payload custody or garbage collection | Usually no | Only if consensus-visible execution changes. |
| Change snapshot metadata validation | Maybe | It depends whether restored/replayed chain semantics change. |
| Change sidecar handshake or local preflight | Maybe | Sidecar protocol is separate, but deterministic runtime requirements may be protocol-pinned. |
If old blocks could produce a different state root after your change, add a new protocol version.
Implementation Map
Protocol versioning is implemented across a small set of core files.
Protocol definitions and lookup:
src/protocol/protocol-types.ts:ProtocolDefinition,ProtocolTransactionHandler,ProtocolExecutionContext, and deterministic policy types.src/protocol/protocol-registry.ts: supported protocol registry and height-based lookup.src/protocol/versions/v1.ts: protocol version 1 definition.src/protocol/versions/latest.ts: latest protocol definition used by the binary when selecting the default/latest supported version.
Persistence and configuration:
src/db/migrations/007_block_protocol_version.sql: addsblocks.protocol_versionandprotocol_upgrades.src/config/genesis.ts: validatesGenesisConfig.protocolVersion.src/protocol/protocol-info.ts: builds operator-facing protocol status.
Execution, sync, and replay:
src/chain/transaction-executor.ts: dispatches block execution through the selected protocol definition.src/node-sync/remote-block-validation.ts: validates remote block protocol support before sync inserts and executes blocks.src/replay/replay-engine.ts: replays blocks with their persistedprotocol_versionand reports protocol mismatches.src/tools/verify-protocol-versioning.ts: verifies registry and persisted protocol metadata.
Snapshots and references:
src/snapshots/*: exports, imports, streams, and verifies snapshot protocol metadata.- Generated protocol types
- Generated protocol registry
- Generated protocol info
Data Model
blocks.protocol_version is the canonical protocol version for a persisted block. Replay and verification must use it.
protocol_upgrades records height-based activation rows. New block production selects the highest active upgrade whose activation_height is less than or equal to the target height.
Genesis documents include protocolVersion. Genesis validation checks that the configured version is supported by the binary.
Snapshots include protocol metadata so restored or verified state can be tied to the execution semantics that produced it.
computeBlockHash() includes protocol_version. Changing only the protocol version changes the block commitment, even when the transaction list is otherwise identical.
The key distinction:
| Data | Purpose |
|---|---|
blocks.protocol_version | Historical execution version for that exact block. |
protocol_upgrades | Height schedule for future/new block selection. |
Genesis protocolVersion | Initial protocol version for a new chain. |
Snapshot protocolVersion | Protocol metadata for exported/restored state. |
Version Resolution
The registry resolves versions in four different contexts.
getProtocolDefinition(version) returns the compiled protocol definition for a known version. It throws for unknown versions.
getLatestProtocolVersion() returns latestProtocol.version.
getProtocolVersionForHeight(database, height) reads protocol_upgrades and selects the highest active upgrade row at or before height. If no row applies, it falls back to the latest supported version.
getProtocolVersionForNextBlock(database) reads the current chain head, computes the next height, and delegates to getProtocolVersionForHeight.
Treat src/protocol/versions/latest.ts carefully. Changing latest changes what a chain selects when no active protocol_upgrades row applies. That can be fine for a new chain, but it is a compatibility decision for existing chains.
Historical replay must not call latest-version helpers to decide semantics. It must use the block's stored protocol_version.
ProtocolDefinition And Handlers
ProtocolDefinition is the versioned execution unit:
interface ProtocolDefinition {
version: number;
handlers: Map<TransactionType, ProtocolTransactionHandler>;
}Each ProtocolTransactionHandler executes one transaction type under one protocol version. The handler receives:
- the database transaction for the current block execution;
- the persisted transaction row or transaction-shaped object;
- the canonical transaction payload;
- the selected
ProtocolExecutionContext; - the actor resolved from the transaction signer.
ProtocolExecutionContext carries deterministic execution inputs such as:
protocolVersion;- local node id when needed by deterministic handler paths;
- payload availability policy;
- smart-contract package provenance policy;
- smart-contract registry policy;
- smart-contract deactivation policy.
Anything in this context that affects committed state must be stable for every validator and replaying node. Do not smuggle local operator choices into deterministic execution.
Current v1 Shape
src/protocol/versions/v1.ts defines protocolV1.
Current v1 maps supported domain transaction types to handlers created by createHandler(type). Those handlers delegate to executeDomainTransaction with the selected protocol context.
That means v1 is mostly a versioned binding around the current domain-handler semantics. After v1 is used on a chain, do not mutate v1 to mean a new thing. Add a new protocol definition and activate it.
Adding Protocol v2
Use this procedure when deterministic execution behavior changes.
- Create
src/protocol/versions/v2.ts. - Define
protocolV2: ProtocolDefinition. - Copy or compose v1 handlers intentionally.
- Replace only the handlers whose semantics changed.
- Register
protocolV2insrc/protocol/protocol-registry.ts. - Update
src/protocol/versions/latest.tsonly when new/default block selection should use v2. - Decide how the activation row is created in
protocol_upgrades. - Ensure every validator expected to sync or produce post-activation blocks deploys a binary that supports v2 before activation.
- Add version-resolution, handler, replay, sync, snapshot, and activation boundary tests.
- Update docs and generated reference.
Example shape:
import type { ProtocolDefinition } from "../protocol-types.js";
import { protocolV1 } from "./v1.js";
import { executeNewPermissionRules } from "./v2-permissions.js";
export const protocolV2: ProtocolDefinition = {
version: 2,
handlers: new Map(protocolV1.handlers),
};
protocolV2.handlers.set("GRANT_BUCKET_ACCESS", executeNewPermissionRules);This is only a pattern. For major changes, building the handler map explicitly can be safer than inheriting v1 wholesale, because accidental inheritance hides review decisions.
Activation And Rollout
Protocol activation is height-based. The highest active protocol_upgrades row at or before the target block height wins.
A safe rollout should:
- deploy binaries that support the new protocol before the activation height;
- run
npm run verify:protocolon every validator; - confirm observability reports the expected latest supported and active versions;
- avoid proposing blocks with a version unsupported by validators expected to sync or replay the chain;
- document rollback before activation.
Before activation, rollback usually means cancelling or replacing the planned activation through the approved migration/governance path for that environment.
After activation, rollback cannot mean "reinterpret v2 blocks as v1." If v2 blocks have finalized, old binaries that do not support v2 cannot safely continue. Recovery must preserve the historical protocol version and replay semantics.
Activation must not depend on wall-clock time, local operator config, process state, or peer-specific behavior.
Replay Behavior
Replay must dispatch by block.protocol_version.
Do not use latestProtocol, getLatestProtocolVersion(), wall-clock time, local config, or operator flags to decide historical block semantics.
Unsupported historical versions are replay errors. This is intentional: unknown semantics must fail closed rather than silently replaying with the wrong rules.
Replay comparisons include protocol metadata. Verification and conflict paths can report protocol_version_mismatch when the stored block metadata diverges from expected canonical metadata.
Sync Behavior
Sync validates remote protocol versions before trusting a peer's block data.
Remote block validation calls getProtocolDefinition(block.header.protocolVersion). If the local binary does not support that version, sync rejects the block before insertion and execution.
When transaction envelopes include a protocol version, the envelope version must match the block header protocol version.
Sync must never trust a peer's materialized domain tables. It validates block shape, ordering, signatures, Merkle roots, protocol support, block hashes, and state-root compatibility, then executes locally.
Snapshots And Restore
Snapshots carry protocol metadata for the exported head and persisted block rows. Import and verification logic must preserve that metadata.
Snapshot changes need protocol review when they alter:
- which protocol version a restored node uses after import;
- whether unsupported versions are rejected;
- how block metadata is compared during verification;
- whether restored state can replay forward with the same semantics.
A restored node must continue from the imported protocol context. It must not rewrite snapshot metadata to match the current binary's latest protocol.
Adjacent Version Systems
NOOSChain has several version concepts. Keep them separate.
| Version | Selects | Stored where | Example |
|---|---|---|---|
Chain protocol_version | Transaction execution semantics. | blocks.protocol_version, protocol_upgrades, snapshots. | 1, 2 |
| State-root engine version | How materialized state leaves become a state root. | block state-root metadata and state-root activation rows. | iden3-v1, nervos-smt-v2 |
| Smart-contract runtime requirements | Deterministic contract execution requirements for a chain protocol. | contract runtime activation and rollout rows. | wasm-assemblyscript-v1 requirements hash |
| Sidecar protocol version | External sidecar wire/handshake contract. | sidecar config, readiness, checkpoint metadata. | Nervos sidecar protocol 1, contract sidecar protocol 2 |
| Package or manifest version | Contract package and author-facing API versioning. | package descriptors, manifests, registry rows. | package 0.2.0, manifest schema 1 |
A protocol change can use the same state-root engine. A state-root engine activation can preserve the same transaction semantics. A smart-contract runtime change may need a protocol version only when deterministic block execution semantics change.
Related deep pages:
- State-Root Engine Migration
- State-Root Activation Operations
- Smart Contract Runtime Operations
- Smart Contracts
Testing Matrix
Use targeted tests for the changed handler or module, then add broader tests when the change crosses replay, sync, snapshot, runtime, or state-root boundaries.
| Risk | Test evidence |
|---|---|
| Registry lookup changed | Unit tests for getProtocolDefinition, latest version, unknown version errors, and height lookup. |
| Handler semantics changed | Old-version handler tests and new-version handler tests using the same payload where useful. |
| Activation boundary changed | Tests for height before activation, activation height, and height after activation. |
| Replay changed | Replay old blocks through old protocol and new blocks through new protocol. |
| Sync changed | Remote block validation rejects unsupported versions and envelope/header mismatches. |
| Block identity changed | Block hash changes when only protocol_version changes. |
| Snapshot changed | Export/import/verify preserves protocol metadata and rejects unsupported versions. |
| State-root interaction changed | State-root engine version remains separate from protocol version. |
| Contract runtime requirements changed | Runtime lookup fails closed for unknown chain protocol versions and compatibility vectors are updated. |
Baseline commands:
npm run verify:protocol
npm run verify:replay
npm run verify:chain
npm run test-e2e:allUse narrower commands first while developing, then run the broader gates before review.
Observability And Verification
src/protocol/protocol-info.ts builds operator-facing protocol info:
- latest supported version in the current binary;
- active version for the next block;
- configured protocol-upgrade rows ordered by activation height.
Use:
npm run verify:protocol
npm run noos -- observability chain
npm run noos -- node overviewOperators should be able to answer:
- what protocol version this binary supports;
- what protocol version the next block will use;
- what protocol upgrades are configured;
- whether the node can sync and replay the chain it sees.
Anti-Patterns
Avoid these:
- replaying historical blocks with
latestProtocol; - mutating v1 handler semantics after v1 has been used on a chain;
- adding compatibility hacks for old local development database bugs;
- treating unknown versions as latest;
- making activation depend on wall-clock time;
- letting local operator config affect deterministic execution;
- conflating
protocol_versionwithstate_root_engine_version; - conflating chain protocol version with sidecar protocol version;
- changing deterministic failure behavior without a new version;
- hiding a breaking change behind an observability-only flag.
Review Checklist
Before merging protocol-sensitive work, confirm:
- old blocks still replay with their stored protocol version;
- the new behavior is isolated to the intended protocol version;
- unknown versions fail closed;
- activation-height behavior is tested before, at, and after the boundary;
- sync rejects unsupported remote protocol versions;
- snapshots preserve protocol metadata;
- block hash expectations include protocol version;
- state-root engine version and sidecar protocol versions remain separate;
- operator observability exposes active and latest protocol versions;
- docs and generated reference are updated.
Development Chain Warning
Existing local development chains created before block protocol versions may have historical block hashes or failed transactions that do not replay under current code.
Do not preserve these by adding special-case compatibility branches. Reset or reinitialize development databases when you need a clean protocol-versioned chain.