Raft Consensus Adapter Internals
This page is for developers changing the Raft consensus adapter boundary. It is not an operator deployment guide and it is not a general explanation of consensus. For those, see Consensus Overview and Raft Consensus Configuration.
The core rule is:
Raft orders opaque NOOSChain block entries. NOOSChain deterministic execution applies and verifies them.
Raft must never mutate organizations, users, buckets, permissions, encrypted payload metadata, contracts, validator governance state, state-root leaves, or other domain tables directly.
Architecture Boundary
The adapter exists to connect Raft ordering to the normal NOOSChain block executor.
flowchart TD
A["Signed transactions enter local mempool"] --> B["Current leader selects transactions"]
B --> C["Leader builds candidate NOOSChain block"]
C --> D["Candidate is simulated in rollback transaction"]
D --> E["Expected block hash and state root are known"]
E --> F["Raft replicates opaque block DTO entry"]
F --> G["Majority commits log entry"]
G --> H["Each node applies entry via deterministic block executor"]
H --> I["Node recomputes block hash and state root"]
I --> J{"Matches committed entry?"}
J -->|yes| K["Persist applied block and progress"]
J -->|no| L["Record critical conflict and enter incident mode"]The important properties are:
- transaction admission still uses the normal mempool path;
- the leader proposes a NOOSChain block DTO, not raw database mutations;
- candidate simulation computes the hash/root that followers must reproduce;
- Raft replication only orders the DTO;
- committed entries are applied through the same deterministic executor used by replay and verification;
- local execution divergence is a critical incident, not an automatic reorg.
Adapter Contract
The RaftEngine boundary owns consensus mechanics:
- leader election and leader status;
- proposal and replication of opaque log entries;
- committed-entry delivery back to TypeScript;
- local Raft term, vote, log, commit index, and apply index;
- runtime membership view or mutation when the backend supports it;
- consensus health and degraded/dead status reporting.
The TypeScript chain layer owns NOOSChain semantics:
- transaction selection and candidate block construction;
- protocol-version dispatch;
- deterministic domain execution;
- permission, nonce, bucket, contract, and governance handlers;
- state-root calculation and verification;
- snapshots and replay verification;
- conflict and incident recording;
- operator observability.
The replaceable unit is a committed NOOSChain block entry. It is not a database diff, SQL script, materialized domain table dump, or peer-provided state.
Commit Path
The happy path is:
- The leader drains eligible transactions from its local mempool.
- The leader builds a candidate block at the next height.
- The candidate executes in a rolled-back database transaction to compute the deterministic final block hash and state root.
- The leader submits the opaque block entry to the configured Raft engine.
- Raft replicates and commits the entry after quorum.
- Each voter applies the committed entry locally through NOOSChain block execution.
- The node verifies that its computed block hash and state root match the committed entry.
- The node persists apply progress only after deterministic execution succeeds.
This flow is deliberately conservative. Consensus ordering and chain execution stay separate so replay can reproduce finalized history without running the Raft engine.
Noosraft And HashiCorp Raft
RAFT_ENGINE=noosraft uses the TypeScript in-process Raft implementation and the HTTP/in-process transports described on this page.
RAFT_ENGINE=hashicorp-go uses the same conceptual adapter contract, but leader election, log replication, and runtime membership are owned by an external Go sidecar. TypeScript still applies committed NOOSChain block DTOs and verifies hash/root results.
For the HashiCorp implementation details, see HashiCorp Raft Engine. For deployment and operator runbooks, see HashiCorp Raft Deployment.
HTTP Transport
Noosraft can use HTTP transport while keeping the same RaftEngine boundary as the in-process transport.
Outbound RequestVote, AppendEntries, and response messages are serialized as:
{
"message": { "...": "RaftMessage" }
}and posted to:
POST /raft/messageThe route authenticates the node-to-node Raft message and passes it into the local Raft transport handler. It does not execute NOOSChain domain logic and it does not write blocks directly. Blocks are applied only after the Raft engine marks a log entry committed.
Node Authentication
Every Raft HTTP message must include:
x-noos-node-id: <sender-node-id>
x-noos-node-timestamp: <iso timestamp>
x-noos-node-signature: <ed25519 signature>The signed message is:
sha256(method + ":" + path + ":" + timestamp + ":" + bodyHash)where bodyHash is the SHA-256 hash of the canonical JSON request body.
The receiver verifies:
- the sender node exists in
nodes; - the timestamp is fresh;
- the Ed25519 signature matches the sender node public key.
Explicit failures include:
RAFT_NODE_AUTH_INVALID;RAFT_NODE_UNKNOWN;RAFT_NODE_TIMESTAMP_STALE.
This is Raft transport authentication. It is separate from user transaction signatures, API bearer sessions, operator tokens, bucket permissions, and contract authorization.
Persistent Raft State
Noosraft persists local Raft metadata in PostgreSQL.
| Table | Purpose |
|---|---|
raft_node_state | Current term, voted-for node, commit index, and last applied index. |
raft_log_entries | Per-node log entries, terms, entry hashes, committed flags, and applied flags. |
This is consensus-engine metadata. It does not store user private keys, bucket DEKs, plaintext payloads, bearer tokens, or materialized domain state outside normal NOOSChain tables.
Persistence timing matters:
- term and vote changes are persisted before sending vote responses;
- log entries are persisted before an
AppendEntriessuccess response; - commit progress is persisted as entries become committed;
- apply progress is persisted only after the committed block entry executes and verifies through NOOSChain deterministic execution.
On startup, a voter reloads its term, vote, commit index, last applied index, and log entries before participating in elections. If it was offline while another leader committed blocks, it catches up through AppendEntries or snapshot catch-up.
Conflict Rules
Raft may overwrite only uncommitted local entries.
If an incoming leader entry conflicts with a local uncommitted suffix, the adapter truncates the uncommitted suffix and appends the leader's entries.
Committed and applied entries are immutable. If an incoming entry conflicts with committed or applied local history, the node refuses to truncate finalized state.
If a committed entry applies but local deterministic execution computes a different block hash or state root, the node records a critical conflict and enters incident mode. It does not automatically reorg, edit blocks, or continue proposing on top of uncertain history.
For operator response, see Conflict Handling.
Roles
Voters run the Raft state machine, participate in elections, and apply committed entries.
The current leader can build candidate blocks and propose entries. Followers reject direct transaction submission with NOT_LEADER and may include the known leader id so clients can retry.
Observers do not vote, do not propose blocks, and do not build Raft log entries. They sync finalized blocks and execute them locally for read, audit, and verification workflows.
Validator governance state and runtime Raft membership are separate. Governance transactions do not automatically mutate Raft voters. For that boundary, see Raft Membership Reconciliation.
InstallSnapshot Catch-Up
Noosraft supports one-message InstallSnapshot catch-up. When a follower is behind by at least RAFT_SNAPSHOT_THRESHOLD_ENTRIES, the leader can export a NOOSChain consensus_state_only snapshot and send it as a Raft InstallSnapshot message.
The snapshot is a normal NOOSChain SnapshotDocument, not raw database state. It follows the same trust rules as operator snapshot import:
- The follower verifies schema, canonical
snapshotHash, chain references, ciphertext-exclusion rules, and recomputed state root. - The follower imports the verified snapshot as a checkpoint.
- Raft updates
commitIndex,lastApplied, and snapshot baseline metadata:snapshotLastIncludedIndexandsnapshotLastIncludedTerm. - Raft discards local log entries at or below the installed snapshot index.
- Future
AppendEntriescontinue after the checkpoint and must extend the checkpoint block hash/state root through deterministic execution.
Invalid snapshots are rejected before checkpoint import. A tampered, wrong-chain, wrong-root, or schema-invalid snapshot must not mutate NOOSChain consensus state.
The MVP transfers the full snapshot in one message. Chunking and streaming are future work.
Replaceability
The adapter depends on a small Raft engine contract and committed NOOSChain block entries. This keeps the consensus backend replaceable.
Future implementations can use:
- a Rust Raft process;
- another external HashiCorp-compatible bridge;
- OpenBFT;
- CometBFT or another BFT backend.
Any replacement must preserve the same application boundary: consensus orders entries; NOOSChain executes, verifies, snapshots, and replays entries.
Security Boundaries
Keep these authentication and trust boundaries separate:
| Boundary | Purpose |
|---|---|
| Raft node-auth headers | Authenticate node-to-node Raft transport messages. |
| User transaction signatures | Authenticate users, organizations, contracts, and chain-admin actors for transactions. |
NOOS_OPERATOR_TOKEN | Protect operator APIs and administrative diagnostics. |
| TLS/mTLS | Encrypt and authenticate transport channels where configured. |
Raft transport must not receive user private keys, bucket DEKs, plaintext payloads, bearer tokens, or direct database credentials. Raft entries are opaque block DTOs and must be applied only through the deterministic executor.
Raft HTTP transport can use HTTPS/mTLS with NOOS_TLS_* settings. The HashiCorp Raft sidecar has separate HTTP and TCP TLS settings; see HashiCorp Raft Engine and HashiCorp Raft Deployment.
Developer Change Checklist
When changing Raft adapter code, verify:
- committed entries remain opaque NOOSChain block DTOs;
- Raft transport routes do not mutate domain state;
- replay still uses block
protocol_version; - block hash and state-root mismatches fail closed;
- term, vote, log, commit, and apply progress persist before acknowledgements that depend on them;
- restart does not lose committed or applied state;
- only uncommitted suffixes can be truncated;
- snapshot catch-up verifies before import;
- observers cannot propose or vote;
- governance validator changes do not auto-mutate runtime voters;
- observability reports degraded, dead, restarting, and conflict states clearly.
Testing Matrix
Use focused Raft tests first, then broader E2E and hardness tests for changes that affect multi-node behavior.
npm run test:raft
npm run test:raft:http
npm run test:raft:membership
npm run test:raft:snapshot| Test | What it proves |
|---|---|
test:raft | In-process transport, election, log replication, persistence, restart safety, and deterministic apply. |
test:raft:http | Real Fastify servers, HTTP transport, node-auth headers, leader election, failover, restart, and convergence. |
test:raft:membership | Runtime membership paths, operator-approved add/remove voter operations, unsafe quorum reduction rejection, failover, and replay convergence. |
test:raft:snapshot | InstallSnapshot catch-up, checkpoint replay, and rejection of tampered, wrong-chain, and wrong-root snapshots. |
For broader coverage, also use:
npm run test-e2e:hardness-multiple-producers-noosraft
npm run test-e2e:hardness-multiple-producers-hashicorp-go
npm run verify:chain
npm run verify:replayUse HashiCorp-specific tests and operator runbooks when the change crosses into the external sidecar path.
Anti-Patterns
Avoid these:
- applying domain mutations inside Raft transport or message handlers;
- replicating raw SQL, database diffs, or peer materialized state;
- trusting a peer's domain tables instead of executing committed blocks locally;
- truncating committed or applied log entries;
- accepting unsigned, unknown-node, or stale Raft HTTP messages;
- importing raw database snapshots for consensus catch-up;
- allowing observers to propose blocks or vote;
- auto-applying governance validator changes to runtime Raft voters;
- continuing block production after committed hash/root mismatch;
- treating Raft metadata as a substitute for replay verification.