Citrate Consensus, GhostDAG
currentConsensus is how Citrate Network turns many blocks into one agreed history. The ledger is a BlockDAG, so a block may name several parents, and the GhostDAG protocol reads that graph and produces a single order that every honest node computes the same way. This page is for developers and operators who want the mental model first and the audited surface after.
What it is
A single-parent chain is a line. Citrate Network is a graph: each block names a selected parent and, optionally, a few merge parents, so the ledger grows like a grafted orchard rather than a single stem. GhostDAG sorts that growth into a total order so the ledger reads as one history.
The protocol divides every block into two sets. The blue set holds the blocks that agree with the honest majority, judged by a k-cluster rule that tolerates a bounded number of blocks seen in parallel. The red set holds the rest. Walking from genesis to the selected tip and interleaving each block's merge set gives the canonical order. The ordering key is blue score, the cumulative count of a block's blue ancestors. The tip with the highest blue score is the head the network builds on, with deterministic tie-breaking when two tips draw level.
One property matters above all the rest: blue score is recomputed, never trusted from the block header. A block that arrives claiming an inflated blue_score is checked against the feasible range derived from its own ancestry, and a value outside that range is rejected at admission. This is enforced in ghostdag.rs and covered by regression tests.
The default parameters are network constants.
| Parameter | Default | Meaning |
|---|---|---|
k | 18 | k-cluster width, how many parallel blocks are tolerated as blue |
max_parents | 10 | the most parents a block may name |
max_blue_score_diff | 1000 | the blue-score gap a reorg may span |
pruning_window | 100000 | how far back the DAG retains full detail |
finality_depth | 100 | the depth parameter for depth-based finality tracking (see current status) |
The testnet node configuration (node/config/testnet.toml) sets a one-second target, but the measured block interval on the active testnet is about two seconds; see chain parameters and genesis for the cadence of each. For where these blocks come from, see the sequencer; for the broader picture, see the primer.
How to use it
You do not run GhostDAG directly; you read its output. To follow the live order against a running node:
- Ask the node for its current tips, the heads it is building on.
- Read a block and note its blue score; the higher the blue score, the closer to the selected tip.
- Walk the selected-parent chain back from the tip to see the order the network agreed on.
- Check a block's depth behind the selected tip. The deeper it is, the more work a competing branch would need to displace it. There is no protocol finality point today: see current status.
The runnable steps, with the exact JSON-RPC calls, are in read the DAG. To construct the engine in Rust, see the example at the end of this page.
Current status
Confirmation on the testnet is probabilistic: a block gains weight as later blocks build on it. Checkpoint finality is specified, not running. The node constructs CheckpointManager, but no production code path calls propose() yet. FinalityTracker and ChainSelector::with_finality are exercised by tests only. The RPC does not serve the finalized block tag.
The public testnet runs a single block producer operated by Citrate. Stake-gated proposer eligibility is staged: it is off by default and turns on when a validator registry is configured (CITRATE_VALIDATOR_REGISTRY). The one-hundred-member committee with a quorum of sixty-seven, described below, is the target design, not the current state.
If you credit deposits or settle payments against Citrate testnet blocks, choose your own confirmation depth and treat it as a risk decision, not a protocol guarantee. The machine-readable record of these facts is verification/claims.json in citrate-chain (deterministic_checkpoint_finality, consensus_ghostdag).
Reference
The consensus stack lives in core/consensus/. The audited surface follows, each item with its code path.
GhostDAG engine, src/ghostdag.rs
GhostDag::new(params, dag_store), construct the engine over a DAG store.GhostDag::calculate_blue_set(block), compute a block's blue set under the k-cluster rule.GhostDag::calculate_blue_score(block), recompute blue score from the blue set, not from the header.GhostDag::add_block(block), admit a block, update relations and tips.GhostDag::select_tip()andGhostDag::get_tips(), the current best tip and all tips.
A header claiming a blue score outside its feasible range is rejected here (GhostDagError, src/ghostdag.rs:34).
Parameters, src/types.rs
GhostDagParams::default() sets k = 18, max_parents = 10, max_blue_score_diff = 1000, pruning_window = 100000, and finality_depth = 100 (src/types.rs:175).
Proposer election, src/ecvrf.rs, src/vrf.rs
Proposer eligibility uses an elliptic-curve verifiable random function over NIST P-256, specifically ECVRF-P256-SHA256-TAI per RFC 9381 (src/ecvrf.rs:3). Each candidate produces an output bound to their secret key and a public input, so the network can check who was entitled to propose without anyone being able to grind the result.
ecvrf::prove(secret, alpha), RFC 9381 section 5.1.ecvrf::verify(...), RFC 9381 section 5.3.VrfProposerSelector(src/vrf.rs) can apply stake-weighted eligibility on top of the VRF output. The node leaves stake gating off by default; without a configured validator registry the fallback selector checks the VRF proof and key binding, not stake.
Depth-based finality, src/finality.rs
FinalityTracker marks a block final once it sits under enough confirmations, confirmation_depth = 100 by default (FinalityConfig, src/finality.rs:42). FinalityStatus is Finalized, PendingFinalization, or Unfinalized. When a ChainSelector is built with_finality, it refuses a reorganization that would rewrite a block the tracker marked final (src/chain_selection.rs:25). The node does not build it that way today, so this protection is specified, not running.
Committee checkpoint finality, src/checkpoint.rs (specified, not running)
The checkpoint layer is designed to add deterministic finality on top of depth tracking. It is implemented and tested as a library, but no production code path proposes checkpoints yet. CheckpointManager coordinates a deterministically selected committee that signs over (height || block_hash) with ed25519, and the signatures are aggregated. A checkpoint finalizes once a quorum signs (CheckpointState::has_quorum). The chain id is bound into the signed message so a vote on one chain cannot replay onto another (src/checkpoint.rs:90).
CheckpointConfig::default() sets interval = 50 blocks, committee_size = 100, and quorum_threshold = 67, which is two-thirds of one hundred plus one (src/checkpoint.rs:98).
In wall-clock terms under current testnet parameters (measured block time about 2 s), a block is included in about 2 s. Once checkpoints run, one would be due every 50 blocks, about 100 s at the measured block time. Until then there is no finality latency to quote.
Example
Construct the engine and read the order in Rust.
use citrate_consensus::*;
use std::sync::Arc;
let dag_store = Arc::new(DagStore::new());
let params = GhostDagParams::default(); // k=18, max_parents=10, finality_depth=100
let ghostdag = GhostDag::new(params, dag_store.clone());
dag_store.store_block(genesis_block).await?;
ghostdag.add_block(&child_block).await?;
// Deterministic total order from genesis to a tip:
let ordering = TotalOrdering::new(dag_store.clone(), Arc::new(ghostdag));
let order = ordering.get_total_order(tip_hash).await?;
// Depth-based finality (default depth 100):
let tracker = FinalityTracker::with_defaults(dag_store.clone());
let finalized = tracker.update_finality(&tip_hash, tip_height).await?;Design rationale
A graph orders work better than a line under load. When two proposers produce blocks at nearly the same moment, a single-parent chain has to discard one; a BlockDAG keeps both as parents and lets GhostDAG decide their order later. That is why blocks may name up to ten parents and why the target time can sit near one second without the orphan waste a line would suffer.
Two choices guard the ledger. Blue score is recomputed rather than trusted, so a block cannot lie its way to the front by claiming a large score. And finality is designed in layers: depth tracking over one hundred blocks for every block, and committee checkpoints designed to add a signed, deterministic guarantee every fifty blocks. The cost is a checkpoint committee that must be selected and must sign; the benefit, once it runs, is that a settled block is settled by both depth and signature. Today checkpoint finality is specified, not running (see current status).
Failure modes
- A block arriving with a forged
blue_scoreis rejected at admission; ordering ignores the header value and uses the recomputed one. - As designed, a reorganization that would rewrite a finalized block is refused by a finality-aware
ChainSelector, which returns a finality error rather than reverting history. That path is specified, not running, on the testnet. - In the checkpoint design, a checkpoint cannot finalize without a quorum of sixty-seven of one hundred committee signatures, so a minority of the committee cannot force a checkpoint. Chain-id binding stops a valid vote from one network being replayed onto another. Neither applies on the live testnet until checkpoints run.
Access and canon
Public. The GhostDAG model, the parameters, and the audited surface are protocol a developer needs to reason about ordering and finality, and GhostDAG itself is published research. No keys, validator secrets, or private endpoints appear here. ECVRF prove takes a secret key as a parameter; no secret value is documented.
Source and verification
- Source files:
core/consensus/src/types.rs,src/ghostdag.rs,src/ecvrf.rs,src/vrf.rs,src/finality.rs,src/chain_selection.rs,src/checkpoint.rs. - Audited against SHA
9d5959e. - Status: GhostDAG ordering and VRF checks are implemented; checkpoint finality is specified, not running. Pre external audit. The crate is internally tested and TLA+-checked in several areas; it has not completed a third-party audit, so read "tested" as tested, not certified.