# Consensus

> How NeuroChain reaches finality — Malachite (Tendermint) BFT with ML-DSA-65 votes, stake-weighted proposer selection, state-root checks, double-sign slashing, catch-up sync and per-shard instances.

Source: https://docs.nro.world/learn/consensus

NeuroChain runs **Malachite** (Informal Systems) — Tendermint BFT as a Rust library, embedded in the node. A block is **final** as soon as validators holding more than 2/3 of the stake have precommitted it: there are no forks to wait out and no confirmations to count.

| Property | Value |
|----------|-------|
| Algorithm | Tendermint BFT: propose → prevote → precommit |
| Voting power | Stake of the validator, in uNRO |
| Commit threshold | More than 2/3 of total voting power |
| Fault tolerance | Up to `f` faulty validators out of `3f + 1` |
| Vote and proposal signatures | ML-DSA-65 (post-quantum) |
| Finality | Immediate on commit |

## One height, step by step

![One consensus height: the proposer sends a proposal with its TX list, tx_root and previous state root; validators check it, prevote, then precommit; with more than 2/3 of stake precommitting, every validator applies the block; without a quorum in time, the next round starts with a new proposer](/diagrams/consensus-height.svg)

1. **Propose.** The proposer for `(height, round)` builds a block from the mempool: the transaction list, its Merkle root `tx_root`, and `prev_state_root` — the state root it reached after the previous block.
2. **Validate.** Every other validator checks the transactions against `tx_root` and compares `prev_state_root` with its own root for the previous height. A mismatch means one side's state has diverged; the validator refuses to prevote and logs `STATE DIVERGENCE`.
3. **Prevote, precommit.** Validators vote for the block, or for nil if it is invalid or did not arrive in time.
4. **Commit.** With precommits from more than 2/3 of voting power, the block is decided. Each validator applies its transactions, splits the fees, pays staking rewards on reward heights, and computes the new state root — all deterministically, so every honest node ends at the same root.
5. **No quorum in time** moves the height to the next round, with a new proposer and longer timeouts.

Round timeouts are Malachite's defaults. The pace between blocks is set per node:

| Variable | Default | Meaning |
|----------|---------|---------|
| `NEUROCHAIN_BLOCK_MS` | 200 | Minimum wait before proposing a block that carries transactions |
| `NEUROCHAIN_EMPTY_BLOCK_MS` | 2000 | Minimum wait before proposing an empty block, so an idle chain stays quiet |

## Who proposes

The proposer is chosen **by stake alone**, deterministically, so every node computes the same answer:

1. Take the active validators and their stakes; divide all stakes by their greatest common divisor, so only proportions matter.
2. Hash `SHA3-256("neuro-proposer|" ‖ height ‖ round)` to get a number spread evenly over the total weight.
3. Walk the validators in order; the one whose weight range contains that number proposes.

A validator with twice the stake proposes twice as often. A round change picks a fresh number, so a validator that is down is skipped. If no validator has any stake left, selection falls back to round-robin so the chain keeps moving.

## Validator set

A validator takes part in consensus when **all** of these hold:

- Its address ends in `.node`, and its ML-DSA-65 consensus public key is on chain.
- Its stake is at least `min_stake` (default 10 000 NRO).
- It is not slashed and not jailed.
- In a sharded network, it belongs to this shard.

The set is re-derived from chain state, so stake changes, slashes and new registrations take effect through transactions every validator has applied.

The protocol needs at least **7 validators** (`f = 2`) for real Byzantine fault tolerance. A node with fewer logs a warning at start; `GET /health` reports `validator_count`, `bft_quorum` (the `2f + 1` count for the current set) and `bft_ready`. Override the minimum for test networks with `NEUROCHAIN_MIN_VALIDATORS`.

## Where AI may and may not act

The safety proof of Tendermint rests on voting power being stake. AI must never change it.

| | Allowed? | Status |
|---|---|---|
| Changing voting power or the 2/3 threshold | **Never** | Enforced: votes are counted by Malachite from stake |
| Removing a validator (jail, slash → voting power 0) | Yes — removal only, never extra weight | Live, triggered by double-sign evidence |
| Biasing proposer selection by trust | Yes in principle | **Not active**: proposer choice is stake-only |

**ConsensusAI** computes a trust score for each validator from its recent participation. Today that score is **reported, not used**: you see it in `GET /api/v1/validators` and in the oracle decision log for every block. It was taken out of proposer selection because each node counted participation from its own view; after an outage those views drifted apart, nodes disagreed on who may propose, and never re-converged. Bringing trust back into proposer choice needs participation data that is itself agreed in consensus (the previous block's commit carried inside the next block).

## Slashing and jail

Signing two different votes for the same height and round is provable misbehaviour. When such evidence is submitted and verified in consensus:

| Effect | Default | Governance parameter |
|--------|---------|----------------------|
| Stake slashed | 5% of `min_stake` (capped at the validator's stake) | `slash_fraction_double_sign_bps` = 500 |
| Reporter reward | 1% of the slashed amount | `slash_reward_fraction_bps` = 100 |
| Remainder | burned | — |
| Jail (voting power 0) | 10 000 blocks | `jail_blocks` (env `NEUROCHAIN_JAIL_BLOCKS`) |

Each piece of evidence is applied once. After the jail period the validator returns to the set automatically, provided its stake is still at least `min_stake`. Slashes are listed at `GET /api/v1/slashing/history`.

## Restarts and catch-up

- **Restart.** Malachite keeps a write-ahead log. A restarted validator replays it and may decide a height it had already committed; the node recognises the block as applied and carries on instead of halting.
- **Catch-up.** A node that was offline asks peers for the decided blocks it missed — each with its commit certificate and full transaction list — and applies them in order, exactly as if it had taken part. Peers keep the last 10 000 decided blocks for this (`NEUROCHAIN_SYNC_HISTORY_BLOCKS`). A node further behind than that stays visibly behind rather than copying block records it has not applied; the old block-record sync now only compares state roots for blocks the node already holds.
- **Fresh node.** A new node can start from a signed state snapshot instead of replaying from genesis — see [Run a node](/operate/node).

## Sharding <Badge type="warning" text="experimental" />

A network can be split into shards, each running **its own Malachite instance** with its own validators and block height:

- An account or validator belongs to shard `FNV-1a(address) mod n_shards`.
- A node serves the shard in `NEUROCHAIN_SHARD_ID`; the shard count is `NEUROCHAIN_SHARDS`. Without `NEUROCHAIN_SHARD_ID`, the node runs a single shard and there are no cross-shard transfers.
- A transfer to another shard is a two-phase commit: the source shard **locks** the value; the destination shard **commits** it after checking a Merkle proof of the debit; if that has not happened within 10 blocks (`RELAY_TIMEOUT_BLOCKS`), the source **refunds** the sender.

**How it is tested.** Unit tests in the node cover shard assignment, shard-filtered mempool and votes, the lock → commit path and the timeout refund (`test_cross_shard_2pc_commit`, `test_cross_shard_2pc_timeout_refund`, run with `cargo test -p neurochain-node`). Multi-validator network runs so far used a single shard; a run with several shards and several validators per shard has not yet been done. See [IBC v2 & sharding](/operate/ibc) for operation.

## Check it yourself

```bash
curl -s "$NODE_RPC/health"                # height, round, peers, validator_count, bft_quorum, bft_ready
curl -s "$NODE_RPC/api/v1/validators"     # stake, trust score, blocks proposed, votes cast
curl -s "$NODE_RPC/api/v1/blocks?limit=1" # last block: proposer, tx_root, state_root
```
