Skip to content

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.

ChangeNew protocol version?Reason
Change signer or actor resolutionYesThe same transaction could be authorized as a different actor.
Change nonce behaviorYesReplay could accept, reject, or order effects differently.
Change permission checks or implication rulesYesThe same payload could succeed or fail differently.
Change bucket creation, bucket keys, or encrypted-record semanticsYesCommitted state and state roots can change.
Add, remove, or change a deterministic transaction handlerYesHandler dispatch is protocol behavior.
Change deterministic failure behaviorYesFailed transactions are still part of replay semantics.
Change smart-contract runtime requirements used during block executionYesContract calls can produce different results.
Change registry, provenance, or deactivation policy that affects committed executionYesPackage and contract validity can change.
Refactor handler code with byte-for-byte equivalent effectsUsually noProve equivalence with tests.
Optimize performance without changing validation, effects, hashes, or failuresUsually noExecution result is unchanged.
Add observability-only fieldsUsually noOperator visibility is not block semantics.
Change logs, CLI formatting, Admin GUI, or docsNoNo deterministic block output changes.
Change local payload custody or garbage collectionUsually noOnly if consensus-visible execution changes.
Change snapshot metadata validationMaybeIt depends whether restored/replayed chain semantics change.
Change sidecar handshake or local preflightMaybeSidecar 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: adds blocks.protocol_version and protocol_upgrades.
  • src/config/genesis.ts: validates GenesisConfig.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 persisted protocol_version and reports protocol mismatches.
  • src/tools/verify-protocol-versioning.ts: verifies registry and persisted protocol metadata.

Snapshots and references:

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:

DataPurpose
blocks.protocol_versionHistorical execution version for that exact block.
protocol_upgradesHeight schedule for future/new block selection.
Genesis protocolVersionInitial protocol version for a new chain.
Snapshot protocolVersionProtocol 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:

ts
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.

  1. Create src/protocol/versions/v2.ts.
  2. Define protocolV2: ProtocolDefinition.
  3. Copy or compose v1 handlers intentionally.
  4. Replace only the handlers whose semantics changed.
  5. Register protocolV2 in src/protocol/protocol-registry.ts.
  6. Update src/protocol/versions/latest.ts only when new/default block selection should use v2.
  7. Decide how the activation row is created in protocol_upgrades.
  8. Ensure every validator expected to sync or produce post-activation blocks deploys a binary that supports v2 before activation.
  9. Add version-resolution, handler, replay, sync, snapshot, and activation boundary tests.
  10. Update docs and generated reference.

Example shape:

ts
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:protocol on 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.

VersionSelectsStored whereExample
Chain protocol_versionTransaction execution semantics.blocks.protocol_version, protocol_upgrades, snapshots.1, 2
State-root engine versionHow materialized state leaves become a state root.block state-root metadata and state-root activation rows.iden3-v1, nervos-smt-v2
Smart-contract runtime requirementsDeterministic contract execution requirements for a chain protocol.contract runtime activation and rollout rows.wasm-assemblyscript-v1 requirements hash
Sidecar protocol versionExternal sidecar wire/handshake contract.sidecar config, readiness, checkpoint metadata.Nervos sidecar protocol 1, contract sidecar protocol 2
Package or manifest versionContract 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:

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.

RiskTest evidence
Registry lookup changedUnit tests for getProtocolDefinition, latest version, unknown version errors, and height lookup.
Handler semantics changedOld-version handler tests and new-version handler tests using the same payload where useful.
Activation boundary changedTests for height before activation, activation height, and height after activation.
Replay changedReplay old blocks through old protocol and new blocks through new protocol.
Sync changedRemote block validation rejects unsupported versions and envelope/header mismatches.
Block identity changedBlock hash changes when only protocol_version changes.
Snapshot changedExport/import/verify preserves protocol metadata and rejects unsupported versions.
State-root interaction changedState-root engine version remains separate from protocol version.
Contract runtime requirements changedRuntime lookup fails closed for unknown chain protocol versions and compatibility vectors are updated.

Baseline commands:

powershell
npm run verify:protocol
npm run verify:replay
npm run verify:chain
npm run test-e2e:all

Use 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:

powershell
npm run verify:protocol
npm run noos -- observability chain
npm run noos -- node overview

Operators 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_version with state_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.

Audience-first NOOSChain documentation.