# 합의

> NeuroChain이 최종성에 도달하는 방식 — ML-DSA-65 투표를 사용하는 Malachite(Tendermint) BFT, 스테이크 가중 제안자 선택, 상태 루트 검사, 이중 서명 slashing, 따라잡기 동기화, 샤드별 인스턴스를 설명합니다.

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

NeuroChain은 **Malachite**(Informal Systems)를 실행합니다 — 노드에 내장된 Rust 라이브러리 형태의 Tendermint BFT입니다. 스테이크의 2/3를 초과하는 검증자가 precommit하는 즉시 블록은 **최종 확정**됩니다: 포크가 해소되기를 기다릴 필요도, 확인 횟수를 셀 필요도 없습니다.

| 속성 | 값 |
|------|----|
| 알고리즘 | Tendermint BFT: propose → prevote → precommit |
| 투표권 | 검증자의 스테이크, uNRO 단위 |
| 커밋 임계값 | 전체 투표권의 2/3 초과 |
| 장애 허용 | `3f + 1` 중 최대 `f`개의 결함 검증자 |
| 투표와 제안 서명 | ML-DSA-65 (양자 내성) |
| 최종성 | 커밋 즉시 |

## 한 높이의 단계별 진행

![한 합의 높이: 제안자가 TX 목록, tx_root, 이전 상태 루트를 담은 제안을 보내고, 검증자들은 이를 검사해 prevote한 뒤 precommit합니다. 스테이크의 2/3 초과가 precommit하면 모든 검증자가 블록을 적용하고, 제시간에 쿼럼에 도달하지 못하면 새 제안자로 다음 라운드가 시작됩니다](/diagrams/ko/consensus-height.svg)

1. **Propose.** `(height, round)`의 제안자가 mempool에서 블록을 만듭니다: 트랜잭션 목록, 그 Merkle 루트인 `tx_root`, 그리고 `prev_state_root` — 이전 블록 이후 도달한 상태 루트입니다.
2. **검증.** 다른 모든 검증자는 트랜잭션을 `tx_root`와 대조하고, `prev_state_root`를 이전 높이에 대한 자신의 루트와 비교합니다. 불일치는 어느 한쪽의 상태가 갈라졌다는 뜻이며, 검증자는 prevote를 거부하고 `STATE DIVERGENCE`를 로그에 남깁니다.
3. **Prevote, precommit.** 검증자들은 블록에 투표하거나, 블록이 유효하지 않거나 제시간에 도착하지 않으면 nil에 투표합니다.
4. **커밋.** 투표권의 2/3를 초과하는 precommit이 모이면 블록이 결정됩니다. 각 검증자는 트랜잭션을 적용하고, 수수료를 나누고, 보상 높이에서는 스테이킹 보상을 지급하고, 새 상태 루트를 계산합니다 — 모두 결정적으로 이루어지므로 모든 정직한 노드는 같은 루트에 도달합니다.
5. **제시간에 쿼럼이 없으면** 높이는 새 제안자와 더 긴 타임아웃으로 다음 라운드로 넘어갑니다.

라운드 타임아웃은 Malachite의 기본값입니다. 블록 사이의 간격은 노드별로 설정합니다:

| 변수 | 기본값 | 의미 |
|------|--------|------|
| `NEUROCHAIN_BLOCK_MS` | 200 | 트랜잭션이 있는 블록을 제안하기 전 최소 대기 시간 |
| `NEUROCHAIN_EMPTY_BLOCK_MS` | 2000 | 빈 블록을 제안하기 전 최소 대기 시간. 유휴 상태의 체인이 조용히 유지되도록 함 |

## 누가 제안하는가

제안자는 **스테이크만으로** 결정적으로 선택되므로, 모든 노드가 같은 답을 계산합니다:

1. 활성 검증자와 그 스테이크를 가져와, 모든 스테이크를 최대공약수로 나눕니다. 비율만 중요해지도록 하기 위해서입니다.
2. `SHA3-256("neuro-proposer|" ‖ height ‖ round)`를 해시하여 전체 가중치에 고르게 분포된 수를 얻습니다.
3. 검증자를 순서대로 훑어, 그 수가 포함된 가중치 구간을 가진 검증자가 제안합니다.

스테이크가 두 배인 검증자는 두 배 더 자주 제안합니다. 라운드가 바뀌면 새로운 수를 뽑으므로, 다운된 검증자는 건너뛰어집니다. 스테이크가 남은 검증자가 하나도 없으면 체인이 계속 진행되도록 선택이 라운드 로빈으로 전환됩니다.

## 검증자 세트 {#validator-set}

검증자는 다음이 **모두** 충족될 때 합의에 참여합니다:

- 주소가 `.node`로 끝나고, ML-DSA-65 합의 공개 키가 체인에 등록되어 있습니다.
- 스테이크가 `min_stake`(기본값 10 000 NRO) 이상입니다.
- slash되지 않았고 jail 상태가 아닙니다.
- 샤딩된 네트워크에서는 이 샤드에 속합니다.

세트는 체인 상태로부터 다시 도출되므로, 스테이크 변경, slash, 새 등록은 모든 검증자가 적용한 트랜잭션을 통해 반영됩니다.

실질적인 비잔틴 장애 허용을 위해 프로토콜에는 최소 **7개의 검증자**(`f = 2`)가 필요합니다. 그보다 적은 노드는 시작 시 경고를 기록합니다. `GET /health`는 `validator_count`, `bft_quorum`(현재 세트에 대한 `2f + 1` 수), `bft_ready`를 보고합니다. 테스트 네트워크에서는 `NEUROCHAIN_MIN_VALIDATORS`로 최솟값을 재정의할 수 있습니다.

## AI가 관여할 수 있는 곳과 없는 곳

Tendermint의 안전성 증명은 투표권이 스테이크라는 사실에 기반합니다. AI는 절대 이를 바꿔서는 안 됩니다.

| | 허용 여부 | 상태 |
|---|---|---|
| 투표권 또는 2/3 임계값 변경 | **절대 불가** | 강제됨: 투표는 Malachite가 스테이크 기준으로 집계 |
| 검증자 제외 (jail, slash → 투표권 0) | 허용 — 제외만, 추가 가중치는 절대 불가 | 동작 중, 이중 서명 증거로 발동 |
| 신뢰도로 제안자 선택에 편향 주기 | 원칙적으로 허용 | **비활성**: 제안자 선택은 스테이크만 사용 |

**ConsensusAI**는 각 검증자의 최근 참여도로부터 신뢰 점수를 계산합니다. 현재 이 점수는 **보고만 되고 사용되지 않습니다**: `GET /api/v1/validators`와 모든 블록의 오라클 결정 로그에서 볼 수 있습니다. 이 점수가 제안자 선택에서 빠진 이유는 각 노드가 참여도를 자신의 관점에서 계산했기 때문입니다. 장애 이후 이 관점들이 서로 어긋났고, 노드들은 누가 제안할 수 있는지에 합의하지 못했으며, 다시 수렴하지 않았습니다. 신뢰도를 제안자 선택에 다시 도입하려면 참여도 데이터 자체가 합의로 합의되어야 합니다(이전 블록의 커밋을 다음 블록 안에 담아 전달).

## Slashing과 jail {#slashing-and-jail}

같은 높이와 라운드에 대해 서로 다른 두 투표에 서명하는 것은 증명 가능한 부정행위입니다. 그런 증거가 제출되어 합의에서 검증되면:

| 효과 | 기본값 | 거버넌스 파라미터 |
|------|--------|-------------------|
| 스테이크 slash | `min_stake`의 5% (검증자의 스테이크가 상한) | `slash_fraction_double_sign_bps` = 500 |
| 신고자 보상 | slash된 금액의 1% | `slash_reward_fraction_bps` = 100 |
| 나머지 | 소각 | — |
| Jail (투표권 0) | 10 000 블록 | `jail_blocks` (환경 변수 `NEUROCHAIN_JAIL_BLOCKS`) |

각 증거는 한 번만 적용됩니다. jail 기간이 끝나면 스테이크가 여전히 `min_stake` 이상인 한 검증자는 자동으로 세트에 복귀합니다. slash 내역은 `GET /api/v1/slashing/history`에서 확인할 수 있습니다.

## 재시작과 따라잡기

- **재시작.** Malachite는 선행 기록 로그(write-ahead log)를 유지합니다. 재시작된 검증자는 이를 재생하며, 이미 커밋한 높이를 다시 결정할 수 있습니다. 노드는 그 블록이 이미 적용되었음을 인식하고 멈추지 않고 계속 진행합니다.
- **따라잡기.** 오프라인이었던 노드는 놓친 결정된 블록 — 각각 커밋 인증서와 전체 트랜잭션 목록 포함 — 을 피어에게 요청하고, 마치 직접 참여했던 것처럼 순서대로 적용합니다. 이를 위해 피어들은 마지막 10 000개의 결정된 블록을 보관합니다(`NEUROCHAIN_SYNC_HISTORY_BLOCKS`). 그보다 더 뒤처진 노드는 적용하지 않은 블록 기록을 복사하는 대신 뒤처진 상태가 눈에 보이게 남습니다. 예전의 블록 기록 동기화는 이제 노드가 이미 보유한 블록에 대해 상태 루트를 비교하는 데만 쓰입니다.
- **새 노드.** 새 노드는 genesis부터 재생하는 대신 서명된 상태 스냅샷에서 시작할 수 있습니다 — [노드 실행](/ko/operate/node)을 참고하세요.

## 샤딩 <Badge type="warning" text="실험적" /> {#sharding}

네트워크는 여러 샤드로 나눌 수 있으며, 각 샤드는 자체 검증자와 블록 높이를 가진 **자체 Malachite 인스턴스**를 실행합니다:

- 계정이나 검증자는 샤드 `FNV-1a(address) mod n_shards`에 속합니다.
- 노드는 `NEUROCHAIN_SHARD_ID`의 샤드를 담당하며, 샤드 수는 `NEUROCHAIN_SHARDS`입니다. `NEUROCHAIN_SHARD_ID`가 없으면 노드는 단일 샤드로 실행되고 크로스 샤드 전송은 없습니다.
- 다른 샤드로의 전송은 2단계 커밋입니다: 출발 샤드가 금액을 **잠그고**, 도착 샤드가 차감에 대한 Merkle 증명을 확인한 뒤 이를 **커밋**합니다. 10 블록(`RELAY_TIMEOUT_BLOCKS`) 안에 이루어지지 않으면 출발 샤드가 발신자에게 **환불**합니다.

**테스트 방법.** 노드의 단위 테스트가 샤드 할당, 샤드별로 필터링된 mempool과 투표, 잠금 → 커밋 경로, 타임아웃 환불을 다룹니다(`test_cross_shard_2pc_commit`, `test_cross_shard_2pc_timeout_refund`, `cargo test -p neurochain-node`로 실행). 지금까지 다중 검증자 네트워크 실행은 단일 샤드를 사용했으며, 여러 샤드에 샤드당 여러 검증자를 둔 실행은 아직 이루어지지 않았습니다. 운영 방법은 [IBC v2와 샤딩](/ko/operate/ibc)을 참고하세요.

## 직접 확인하기

```bash
curl -s "$NODE_RPC/health"                # 높이, 라운드, 피어, validator_count, bft_quorum, bft_ready
curl -s "$NODE_RPC/api/v1/validators"     # 스테이크, 신뢰 점수, 제안한 블록, 투표 수
curl -s "$NODE_RPC/api/v1/blocks?limit=1" # 마지막 블록: 제안자, tx_root, state_root
```
