Rand Protocol
문서
핵심 개념

합의와 최종성

노드가 실행하는 Chained HotStuff — 쿼럼, 리더, 연속 세 뷰 커밋 규칙, 타임아웃, 에포크 — 와 testnet에서 클라이언트에게 "최종"이 의미하는 바를 설명합니다.

이 페이지는 트랜잭션이 언제 최종 확정되는지, 검증자(validator)가 몇 개나 가동 중이어야 하는지, 그리고 RPC와 탐색기에서 보게 되는 블록 필드가 무엇을 의미하는지 확인하는 데 사용합니다. 합의는 지분 가중 쿼럼을 사용하는 체인형 HotStuff BFT이며, chain 20에서는 26개 검증자 중 18개가 쿼럼입니다.

노드가 실행하는 규칙

규칙구현 내용
쿼럼쿼럼 인증서(QC)는 전체 지분의 2/3 초과를 보유한 검증자들의 Dilithium2 투표 집합입니다
리더 일정순수 라운드 로빈입니다. 뷰 v의 리더는 validators[v mod n]이며, 집합은 주소순으로 정렬됩니다. 지분은 투표에 가중치를 부여할 뿐 리더 선정에는 영향을 주지 않습니다
제안뷰 v의 리더는 v − 1에 대한 QC를 확보하거나 v에 대한 NewView 메시지의 쿼럼을 확보하면 제안합니다. 유효하지 않은 트랜잭션을 적용하게 되는 제안은 블록 전체가 아니라 해당 트랜잭션만 제외합니다
투표와 안전성검증자는 뷰당 한 번만 투표하며, 자신의 락(lock)에 비추어 안전한 블록에만 투표합니다(해당 블록의 justify QC가 락된 QC보다 최신이거나, 블록이 락된 블록을 확장하는 경우). 안전성 상태는 투표가 노드를 떠나기 전에 RocksDB에 fsync됩니다
서명 도메인consensus_domain: 1인 제네시스(chain 20, 그리고 chain 15 이후의 모든 체인)에서는 모든 투표, new-view, 제안 서명이 제네시스 해시를 포함합니다(rand-vote-2, rand-newview-2, rand-block-2). 따라서 한 체인의 서명은 다른 어떤 체인에서도 검증되지 않습니다. chain 14 및 그 이전에는 이를 포함하지 않고 서명했습니다. chain 20의 binding_domain: 1은 같은 규칙을 트랜잭션 증명과 서명된 액션으로 확장합니다(Transactions)
형제 제안복제본(replica)은 (뷰, 리더)마다 하나의 블록을 보유합니다. 해당 리더가 같은 뷰에 대해 보낸 서로 다른 두 번째 블록은 이중 제안(equivocation)으로 간주되어 거부됩니다. 복제본 자신의 뷰보다 8개 넘게 앞선 제안(PROPOSAL_VIEW_WINDOW)은 서명을 검사하기 전에 거부됩니다. 추측적(speculative) 트리가 가득 찬 경우에는 무언가를 거부하기 전에 다른 분기의 블록을 먼저 제거합니다
락 해제락은 자동으로 낮아지지 않습니다(CON-4, v0.6.8). 락은 justify QC가 락보다 높은 순위인 제안이 오거나, 락된 블록이 도착하는 경우에만 풀립니다. 서명된 NotHeld 응답의 쿼럼은 운영자를 위해 로그로 기록될 뿐 아무것도 해제하지 않으며, 검증자는 자신이 투표한 블록에 대해 NotHeld에 서명하는 일이 없도록 자신의 투표를 기록합니다. 헤드 위의 인증된 블록은 모두 영속화되었다가 재시작 시 복원되므로, 전체 플릿을 재시작해도 아무도 보유하지 않은 블록에 QC가 남겨지지 않습니다. 락된 블록을 영구히 잃은 검증자는 오프라인에서 수동으로 해제합니다: rand-node safety release-lock
QC 조립모든 검증자는 가십(gossip)으로 전파된 투표로부터 QC를 로컬에서 조립하므로, 다음 리더가 오프라인이어도 체인은 계속 커밋됩니다
커밋체인형 3-QC 규칙입니다. 블록 위에 연속된 뷰 v, v+1, v+2로 세 개의 인증서가 연결되고 그 체인이 커밋된 헤드까지 이어질 때 블록이 커밋됩니다
타임아웃과 뷰 변경기본 --view-timeout-ms는 3,000 ms이며, 연속 타임아웃마다 두 배가 되고 진행이 있으면 초기화됩니다. 타임아웃 인증서 페이스메이커(CH-1, v0.6.8)를 사용합니다. 타이머가 만료된 복제본은 자신의 뷰에 머문 채 다음 뷰를 요청하는 NewView에 서명합니다. 복제본은 직전 뷰에 대한 QC가 있거나 지분의 2/3 초과를 보유한 검증자들이 해당 뷰 또는 그 이후의 뷰를 요청했을 때 그 뷰에 진입하며, 자신의 뷰를 넘어서는 뷰를 1/3 초과가 요청하면 합류합니다(자신의 요청에 서명). 단일 검증자의 NewView로는 누구도 이동하지 않습니다. 뷰에는 상한이 있습니다(MAX_VIEW_AHEAD = 10⁶)
페이싱대기 중인 항목이 없으면 리더는 빈 블록을 제안하며, 어떤 제안자로부터든 마지막 블록을 본 시점으로부터 최소 --block-interval-ms(기본 1,000 ms)가 지난 뒤에 제안합니다
투표 및 가십 한도8개 넘게 앞선 뷰의 투표는 무시되며 (뷰, 투표자)당 한 표만 집계됩니다. 합의 가십은 전달하는 피어별로 계량됩니다(버스트 256, 64/s). 부모를 알 수 없는 제안은 제안자가 현재 에포크 집합 또는 다음 에포크 집합에서 해당 뷰의 리더이고, 커밋된 헤드로부터 트리 한도 이내이며, 크기와 tx 루트가 확인되는 경우에만 고아(orphan)로 보관됩니다(고아 풀은 64 MiB로 제한되며, v0.6.1부터는 제안자당 16개로 제한됩니다). v0.6.8부터는 트랜잭션 목록이 헤더가 커밋한 루트로 해시되고 반복되는 트랜잭션이 없는 경우에만 제안을 중계하고 고아로 보관합니다(GOSSIP-2)
블록 한도체인의 max_block_bytes만큼의 트랜잭션 바이트(기본 4 MiB, chain 20의 제네시스에서는 20 MiB)와 2,000개의 트랜잭션이며, 제안자가 적용하고 모든 복제본이 Ledger::apply_block에서 다시 적용합니다
블록 시간bridge 섹션이 있는 체인에서: 블록의 timestamp_ms는 부모의 값보다 앞설 수 없고(TimestampRewind) 부모보다 60초를 초과해 앞설 수도 없으며(TimestampLeap), 검증자는 자신의 시계보다 15초 넘게 앞선 블록에는 투표를 보류합니다. 아래를 참고하십시오

연속된 세 뷰의 커밋 규칙은 안전성을 떠받치는 핵심 요소입니다. 이를 완화하면 상충하는 최종성 버그가 다시 열리며, fullnode에는 이에 대한 회귀 테스트가 있습니다. RPC를 통해 클라이언트가 보는 모든 것은 커밋된 것입니다. 커밋된 블록만 제공되며, WebSocket newHeads 스트림은 커밋된 블록마다 순서대로 알림을 하나씩 보냅니다.

이 표는 chain 20이 실행하는 릴리스인 v0.6.8 기준입니다. 이 가운데 세 가지 규칙은 v0.6.8(audit v6)에서 새로 추가되었으며 chain 20이 가동된 2026-10-01부터 적용 중입니다. 타임아웃 인증서 페이스메이커(CH-1), 자동 락 해제 폐지(CON-4), 제안 트랜잭션 목록 검사(GOSSIP-2)입니다. chain 18과 19(v0.6.7)에서는 단일 검증자가 서명한 NewView가 모든 복제본을 해당 뷰로 이동시켰으며, 지분의 쿼럼이 서명한 NotHeld 응답이 있으면 락이 낮아졌습니다. 페이스메이커와 락 규칙은 검증자가 무엇에 언제 서명하는지를 바꾸므로, v0.6.8은 노드별로 진행하지 않고 chain 20 컷과 함께 전체 중지/전체 시작 방식으로 롤아웃되었습니다.

테스트넷의 수치

chain 20에는 26개의 검증자가 있으며 각각 정확히 1,000 RAND를 스테이킹했습니다. 26개 모두 chain 17~19가 사용한 키와 피어 ID로 제네시스에 포함되어 있습니다. chain 15에서는 chain 14의 키로 18개가 제네시스에 있었고, 8개가 2026-09-27에 v2 등록으로 추가 본딩하여 에포크 43부터 활성화되었습니다. 지분이 균일하므로 쿼럼은 곧 머릿수입니다. 무언가를 커밋하려면 26개 중 18개가 온라인이어야 합니다. 17개가 가동 중이면 뷰는 진행되지만 아무것도 최종 확정되지 않습니다. 동일한 검증자 n개의 집합은 지분의 2/3 초과가 필요합니다. 2개 중 2개, 4개 중 3개, 6개 중 5개, 18개 중 13개, 26개 중 18개입니다. 제네시스 가중치 상한(검증자당 3,333 bps)은 동일한 지분으로 이루어진 집합에는 적용되지 않습니다.

블록은 기본값인 1,000 ms로 페이싱됩니다. chain 15에서는 실제로 약 1.2초마다 도착했으며(처음 29시간 동안 평균 1.18초), chain 20에서는 높이 3,270에서 탐색기가 평균 1.34초를 보고했습니다(2026-10-01, avg_block_time_ms). 탐색기는 자신이 관측한 평균 블록 시간을 보고합니다(GET /stats의 avg_block_time_ms). 뷰가 타임아웃될 때마다 검증자의 뷰는 높이보다 앞서 나가며, 이 때문에 rand_getHead가 둘 다 보고합니다.

에포크

검증자 집합은 epoch_blocks = 1,000 블록마다(제네시스 파라미터) 레지스터에서 다시 도출됩니다. epoch(h) = h / 1000이며, 에포크 e ≥ 1의 집합은 에포크 e − 1의 마지막 블록 시점의 레지스터에서 도출됩니다. stake ≥ 1,000 RAND인 모든 행 중 지분 기준 상위 100개를 주소순으로 정렬합니다. staking 섹션이 없는 체인에서는 본드(bond)가 다음 경계에서 가중치가 됩니다. chain 20에서는 두 에포크를 더 기다려야 하며, 경계 시점의 활성 본딩 가중치의 2,500 bps에 해당하는 에포크당 진입 한도(집합이 26,000을 보유할 때 6,500 RAND)가 적용됩니다. 또한 새 검증자를 등록하는 본드는 집합이 먼저 AdmitValidator로 해당 키를 투표로 승인하지 않으면 거부됩니다(staking.admission_by_vote). chain 19의 한도는 에포크당 10,000 RAND로 고정되어 있었고 승인 투표는 없었습니다. 언본드(unbond)는 다음 경계에서 가중치를 감소시키며, 금액은 두 에포크가 지나야 인출할 수 있게 됩니다. rand_getEpoch는 에포크, 에포크 길이, 그리고 다음 집합의 예상치를 반환합니다. Staking을 참고하십시오.

블록이 담는 내용

{
  "hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
  "proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
  "tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
  "tx_count": 0, "transactions": []
}

view는 블록이 제안된 뷰이고, justify_view는 블록이 부모에 대해 담고 있는 QC의 뷰입니다. timestamp_ms는 제안자가 설정하며, 다음 절의 한도 이내여야 합니다. state_root는 블록 이후의 원장을 커밋합니다. chain 20에서는 다음과 같습니다.

inner      = blake3("rand-state-8" || tree_root || nullifier_root || validators_root || programs_root
                                   || bridge_root || tokens_root
                                   || faucet_epoch || faucet_minted_in_epoch || bond_queue_root
                                   || gas_price || byte_price
                                   || program_state_root)
state_root = blake3("rand-state-admitted-1" || inner || admitted_root)

chain 19는 program_state_root 없이, 래퍼 없이 rand-state-7 아래에서 동일한 필드를 커밋했습니다.

구성 요소는 제네시스에 따라 달라집니다. 선택 섹션이 하나도 없는 체인은 처음 네 가지를 rand-state-2 아래에서 커밋합니다. bridge 섹션이 있으면 bridge_root가 추가됩니다. tokens 섹션이 있으면 그 뒤에 tokens_root가 추가되고 전체 해시의 도메인이 rand-state-4로 바뀝니다. bridge 섹션이 있는 제네시스에는 반드시 tokens 섹션이 있어야 하므로 chain 20에는 둘 다 있습니다. aggregation 섹션이 있으면 그다음에 aggregator 루트가 추가됩니다(tokens가 없는 체인에서는 rand-state-3 아래). chain 20에는 이 섹션이 없습니다. staking 섹션이 있으면 faucet의 에폭 카운터 두 개와 본드 큐의 루트(rand-bond-queue-1)가 맨 끝에 추가되고, 다른 구성과 관계없이 전체 해시의 도메인이 rand-state-5로 바뀝니다. chain 14에는 이 섹션이 없어 rand-state-4 아래에서 해시했고, chain 15~17은 rand-state-5 아래에서 해시했습니다. vesting 섹션이 있으면 그 뒤에 베스팅 레지스터의 루트가 추가되고 해시의 도메인이 rand-state-6으로 바뀝니다. chain 20에는 이 섹션이 없습니다. dynamic 컨트롤러를 쓰는 gas 섹션이 있으면 현재의 gas_price와 byte_price가, 각각 빅엔디언 u64로, 다른 모든 요소 뒤에 추가되고 해시의 도메인이 rand-state-7로 바뀝니다. 이것이 chain 19의 도메인이었으며, chain 20도 가격을 루트에 유지합니다. 가격은 블록마다 한 번, 블록의 트랜잭션 이후 루트 계산 이전에 갱신됩니다(Fees). 고정 가격을 쓰는 gas 섹션은 루트에 포함되지 않습니다. program_state 섹션(RPL-2)이 있으면 가격 뒤에 모든 프로그램의 셀과 볼트의 루트가 추가되고 해시의 도메인이 rand-state-8로 바뀝니다. 이것이 chain 20의 도메인입니다. Audit v6의 스테이킹 상태는 이 루트 안이 아니라 바깥에 감싸는 방식으로 반영됩니다. staking.admission_by_vote(chain 20)에서는 루트가 blake3("rand-state-admitted-1" || inner || admitted_root)이고, staking.slashing(아직 해당하는 체인 없음)에서는 두 번째 래퍼가 jailed 집합을 추가합니다. tokens_root는 blake3("rand-token-leaf-1" || bincode(TokenInfo)) 리프를 인덱스 순서로 놓은 머클 루트이므로, 모든 토큰의 메타데이터, 권한, mint_nonce, 공급량, 그리고 각 백킹의 locked 금액이 합의 상태입니다. 한 토큰 행에 대해 의견이 다른 두 노드는 체인에 대해서도 의견이 다른 것입니다. chain 20에서는 토큰 루트가 레지스트리 확장(max_tokens 상한, 소각된 등록 수수료, 노트 값 상한, 브리지의 롤링 발행 상한 윈도)도 rand-token-registry-3 아래에서 함께 반영합니다. 브리지 루트는 emitter, guardian 집합, 사용된 attestation 다이제스트, 소각 시퀀스, PQ guardian 키, 일시 정지 키, 일시 정지 플래그, 그리고 두 개의 거버넌스 nonce를 포함합니다. 자산 레지스트리는 이 루트에서 빠져 tokens_root로 옮겨졌습니다. chain 20의 브리지에는 rules_v2가 있으므로 로테이션 nonce와 전역 상한 파라미터가 추가되고 도메인은 rand-bridge-state-5입니다(chain 14는 rand-bridge-state-4였습니다). 이 루트는 다시 인바운드 리플레이 하한(rand-bridge-replay-floor-1)으로 감싸지며, chain 20에서는 로테이션 규칙 및 대기 중인 로테이션(rand-bridge-rotation-1)과 수수료 그룹(rand-bridge-fees-1)으로도 감싸집니다. 밸리데이터 레지스터는 validators_root로 해시됩니다(리프 도메인은 chain 20에서 rand-validator-leaf-4이며 각 행의 activation_epoch를 포함하고, staking 섹션이 없는 체인에서는 rand-validator-leaf-2입니다). 따라서 레지스터의 한 행에 대해 의견이 다른 노드는 체인에 대해서도 의견이 다른 것입니다.

블록 시간은 제한됩니다 (B2)

브리지된 체인에서는 블록 타임스탬프가 guardian 집합의 만료 시점과 백킹의 일일 발행 상한이 집계되는 날을 결정합니다. 따라서 시계를 앞으로 건너뛸 수 있는 리더는 로테이션된 집합의 유예 기간을 일찍 만료시키거나 하루치 상한을 건너뛸 수 있습니다. chain 20에는 bridge 섹션이 있으므로 세 가지 규칙이 적용되며, 모두 이 섹션을 조건으로 합니다.

규칙제한종류
되감기 금지timestamp_ms ≥ parent.timestamp_ms(같은 값은 허용), 위반 시 BlockError::TimestampRewind. 제안자는 max(now, parent.timestamp_ms)를 사용합니다유효성 규칙, 리플레이와 동기화 시에도 검사
도약 금지timestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS(60,000 ms), 위반 시 BlockError::TimestampLeap유효성 규칙, 리플레이와 동기화 시에도 검사
시계 편차밸리데이터는 자신의 시계보다 MAX_CLOCK_DRIFT_MS(15,000 ms) 넘게 앞선 블록에 투표하지 않습니다투표 규칙일 뿐입니다. 블록은 투기적 트리에 남아 있고, 시계가 일치하는 정족수는 여전히 이를 인증할 수 있으며, 커밋된 이력을 리플레이할 때는 이 규칙이 적용되지 않습니다

브리지된 체인의 모든 밸리데이터에는 NTP가 필요합니다. 시계가 15초 넘게 어긋난 밸리데이터는 결함 있는 리더(제안이 표를 얻지 못할 수 있음)이자 결함 있는 투표자(정직한 리더의 제때 도착한 블록에 투표를 거부할 수 있음)입니다. 정직한 다수의 시계가 일치하는 한 둘 중 어느 쪽도 체인을 멈추게 하지는 않지만, 동기화되지 않은 플릿은 아무 이유 없이 라이브니스 여유를 잃습니다.

제네시스 timestamp_ms는 실제 런칭 시각에 가까워야 합니다. 과거로 너무 멀면 스텝 제한이 따라잡도록 허용할 때까지 블록 시간이 실제 시계보다 뒤처지며, 따라잡는 속도는 블록당 최대 60초입니다. 미래로 15초를 넘으면 블록 1이 아예 커밋되지 않습니다. 리더 자신의 표를 포함한 모든 표가 실제 시각이 그 시점에 도달할 때까지 보류됩니다. 긴 정지 이후에는 같은 따라잡기 때문에 발행 상한의 하루가 실제 하루보다 빠르게 넘어갈 수 있으며, 그 범위는 누적된 지연 하루당 상한 하루치를 추가로 넘지 않도록 제한됩니다.

클라이언트 관점의 파이널리티

  • rand_getTransaction(hash)가 블록을 반환하면 트랜잭션은 최종 확정된 것입니다. rand_sendTransaction이 수락했다는 것은 멤풀에 들어왔다는 뜻일 뿐입니다.
  • RPC가 제공하거나 익스플로러가 인덱싱한 블록은 커밋된 블록입니다. 기다려야 할 파이널리티 윈도는 없으며, 익스플로러 가이드도 같은 내용을 말합니다.
  • rand_subscribe ["newHeads"]로 받은 헤드는 노드가 잃지 않을 헤드입니다. 블록이 스토리지에 커밋되기 전에는 아무것도 전송되지 않습니다.
  • 뒤처진 노드는 모든 것을 다시 검증합니다. 동기화된 블록을 수락하기 전에 모든 인증서, 모든 번들과 호출 증명, 모든 영수증을 검증합니다. 파이널리티 서명이 증명을 대신해 신뢰되는 일은 없습니다.

풀노드 문서에서 가져온 복구 요약: peer_count가 낮은 상태에서 높이가 진행되지 않으면 네트워킹 문제입니다. 피어가 정상인데 높이가 진행되지 않으면 스테이크의 2/3 미만이 온라인이라는 뜻입니다. 시작 시 CORRUPT CHAIN이 나타나면 노드가 손상된 꼬리 부분을 잘라 내고 재동기화 중이라는 뜻입니다.

fullnode 커밋 c0878c3f(chain 21) 기준으로 2026-10-12에 마지막으로 검증되었습니다. 코드와 이 페이지의 내용이 다를 경우 코드가 옳습니다. 알려 주시기 바랍니다.