NCN CLI
The NCN CLI (Solana Governance Voter Snapshot CLI) is an operator-facing Rust tool for generating stake snapshots (ledger snapshot → MetaMerkleSnapshot) and interacting with the on-chain ncn-snapshot program: initializing config, managing the operator whitelist, casting and removing votes, finalizing ballots, setting tie-breakers, and inspecting on-chain state.
Operator startup (snapshot-first checklist)
If you are onboarding an operator, start from this flow before running voting commands:
- Run and sync a validator/RPC machine to chain tip.
- Use startup settings that keep snapshots available for NCN snapshot generation.
- Generate a snapshot and MetaMerkle artifact.
- Log the Merkle root/hash and verify outputs.
- Upload to verifier service and fetch proof/meta endpoints to validate end-to-end availability.
Recommended validator/RPC startup settings
These values are the baseline used for NCN operator testing:
| Setting | Value |
|---|---|
| Agave version | ^3.0.6 |
| Ledger limit (shred count) | 200000000 |
| Full snapshots retention | 2 |
| Full snapshots interval | 100000 |
| Incremental snapshots retention | 30 |
| Incremental snapshots interval | 2500 |
| Open files limit | LimitNOFILE=2000000 |
| Locked memory limit | LimitMEMLOCK=2000000000 |
Snapshot loading uses io_uring with registered (fixed) buffers, which requires locking up to
~2GB via RLIMIT_MEMLOCK. If LimitMEMLOCK isn’t set this high wherever ncn/cli runs (a
systemd unit, or the login session if run interactively), the tool falls back to unregistered
buffers automatically. This matches the recommended validator’s own limits.
For verifier deployment and API checks, use Verifier Service. This page focuses on CLI commands and operator snapshot flow.
Installation
git clone https://github.com/solana-foundation/solana-governance.git
cd solana-governance
make install-ncn-cli Global Options
These options are available on all commands. Each can also be set via the matching environment variable.
| Option | Description |
|---|---|
--payer-path <PATH> | Keypair file that pays transaction fees and rent. Used as the fee payer for every on-chain instruction issued by the CLI. Does not need to match the program authority or operator authority. |
--authority-path <PATH> | Keypair file that signs privileged actions. Only consulted by subcommands that submit on-chain transactions or sign off-chain messages. The exact role it plays is documented per subcommand (see <subcommand> --help). Read-only and snapshot-only subcommands ignore it. The default / is a placeholder and must be overridden whenever the chosen subcommand actually needs to sign. |
--operator-address <PUBKEY> | Operator pubkey (base58) stamped into generated snapshot metadata so downstream tooling can attribute a snapshot to a specific operator. Defaults to the system program address as a placeholder. |
--rpc-url <URL> | Solana JSON-RPC endpoint used for all on-chain interactions. Examples: https://api.mainnet-beta.solana.com, https://api.devnet.solana.com, or a local validator URL. Default: http://localhost:8899. |
--ledger-path <PATH> | Path to a Solana validator ledger directory (the validator’s --ledger directory containing rocksdb and snapshots). Required by snapshot-related subcommands (snapshot-slot, generate-meta-merkle, await-snapshot). |
--full-snapshots-path <PATH> | Directory containing full Solana ledger snapshots (snapshot-<slot>-<hash>.tar.zst). Defaults to --ledger-path when not provided. Used by snapshot-slot to locate the base full snapshot to replay from. |
--account-paths <PATHS> | One or more accounts-db directories used during ledger replay. Pass a comma-separated list to spread accounts across multiple disks. When omitted, the ledger directory is used as the single accounts path. |
--backup-snapshots-dir <PATH> | Directory where generated snapshots (full + incremental) are written and read. snapshot-slot writes new snapshots here, and generate-meta-merkle expects to find the snapshot for the target slot in this directory. |
--cluster <CLUSTER> | Solana cluster name passed through to bank loading. One of mainnet, devnet, testnet, development. Affects cluster-specific feature activation during ledger replay. Default: mainnet. |
--micro-lamports <N> | Optional priority fee (in micro-lamports per compute unit) attached to outgoing transactions. When set, a ComputeBudgetInstruction::SetComputeUnitPrice is prepended to each transaction. Useful for landing transactions during periods of congestion. |
Environment variable: NCN_SNAPSHOT_MAX_MB — max decompressed snapshot size in MiB (default: 256)
Snapshot Commands
snapshot-slot
Generates a Solana ledger snapshot for a specific target slot. Replays the validator bank up to --slot and writes a full snapshot into --backup-snapshots-dir. Requires --ledger-path and --full-snapshots-path (or defaults derived from --ledger-path).
ncn-cli \
--ledger-path /mnt/ledger \
--full-snapshots-path /mnt/ledger/full-snapshots \
--backup-snapshots-dir /mnt/ledger/backup-snapshots \
snapshot-slot --slot <SLOT>| Argument | Required | Description |
|---|---|---|
--slot <u64> | Yes | Target slot to snapshot. Must be a slot that has already been rooted in the ledger. |
Snapshot path flags
--full-snapshots-path: Where the CLI reads full snapshots from.--backup-snapshots-dir: Where snapshot artifacts are copied/saved for backup.
generate-meta-merkle
Builds a MetaMerkleSnapshot from an existing full ledger snapshot. Loads the bank at --slot from --backup-snapshots-dir and emits a compressed meta_merkle-<slot>.zip containing the stake-weighted merkle tree used for on-chain voting.
ncn-cli \
--ledger-path /mnt/ledger \
--account-paths /mnt/accounts \
--backup-snapshots-dir /mnt/ledger/backup-snapshots \
generate-meta-merkle \
--slot 300000000 \
--save-path ./meta_merkle/| Argument | Required | Default | Description |
|---|---|---|---|
--slot <u64> | Yes | — | Target slot. A full snapshot for this exact slot must already exist in --backup-snapshots-dir. |
--save-path <PATH> | No | ./ | Directory to write the compressed MetaMerkleSnapshot (meta_merkle-<slot>.zip) to. |
log-meta-merkle-hash
Prints the merkle root, snapshot hash, slot, and a signature over them from a MetaMerkleSnapshot file. Useful for sharing the snapshot identity with other operators before voting on-chain. --authority-path signs an off-chain message containing the snapshot slot and meta merkle root; no on-chain transaction is sent.
ncn-cli \
--authority-path ~/.config/solana/id.json \
log-meta-merkle-hash \
--read-path ./meta_merkle-300000000.zip \
--is-compressed true| Argument | Required | Description |
|---|---|---|
--read-path <PATH> | Yes | Path to a MetaMerkleSnapshot file (compressed .zip or raw). |
--is-compressed <bool> | No | Set to true when --read-path points to a compressed .zip produced by generate-meta-merkle (default: true). |
await-snapshot
Waits for a target slot to pass and snapshots it from on-disk snapshots. Polls --snapshots-dir until a full + incremental snapshot pair covering --slot is available, copies them and the relevant ledger range into the backup directories, replays to produce a snapshot at --slot, and optionally generates the MetaMerkleSnapshot. This is the primary command used by automated operator infrastructure.
Important
Always create a new backup directory for
--backup-snapshots-dir.
Do not point it to the source snapshots directory (for example, do not reuse/mnt/snapshots).
Initial setup (once)
# Create backup ledger directory
mkdir -p /mnt/ledger/gov-ledger-backup
mkdir -p /mnt/ledger/gov-ledger-backup/snapshots
# Copy genesis into backup ledger
cp /mnt/ledger/genesis.bin /mnt/ledger/gov-ledger-backup/genesis.binRun await-snapshot
ncn-cli \
await-snapshot \
--scan-interval 1 \
--slot <SLOT> \
--snapshots-dir /mnt/ledger/snapshots \
--backup-snapshots-dir /mnt/ledger/gov-ledger-backup/snapshots \
--backup-ledger-dir /mnt/ledger/gov-ledger-backup \
--agave-ledger-tool-path /home/sol/.local/share/solana/install/active_release/bin/agave-ledger-tool \
--ledger-path /mnt/ledger \
--generate-meta-merkle| Argument | Required | Default | Description |
|---|---|---|---|
--scan-interval <minutes> | Yes | — | Polling interval (in minutes) between snapshot directory scans. |
--slot <u64> | Yes | — | Target slot to snapshot once it has been rooted. |
--snapshots-dir <PATH> | Yes | — | Directory to scan for live validator snapshots (read-only input). |
--backup-snapshots-dir <PATH> | Yes | — | Directory into which the matching full + incremental snapshots are copied and the new snapshot at --slot is written. |
--backup-ledger-dir <PATH> | Yes | — | Directory into which the relevant ledger range is copied via agave-ledger-tool. |
--agave-ledger-tool-path <PATH> | Yes | — | Absolute path to the agave-ledger-tool binary used to copy the ledger range. |
--ledger-path <PATH> | Yes | — | Path to the live validator ledger directory (the same path passed to the validator’s -l/--ledger flag). |
--generate-meta-merkle | No | — | Flag: also generate the MetaMerkleSnapshot after the full snapshot at --slot has been written. |
Program Config Commands
init-program-config
Initializes the on-chain ProgramConfig singleton. Must be run once per deployment. The signer of --authority-path becomes the initial program authority and is recorded as ProgramConfig.authority. --svmgov-program-id binds which svmgov governance program is allowed to open ballot boxes (it is verified in init_ballot_box when a proposal reaches consensus and CPIs in); source it from networks.toml’s svmgov_program_id.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
init-program-config \
--svmgov-program-id <PUBKEY>| Argument | Type | Description |
|---|---|---|
--svmgov-program-id <Pubkey> | Required | svmgov governance program id (base58) whose Proposal PDAs are authorized to open ballot boxes. Recorded as ProgramConfig.svmgov_program_pubkey. |
update-program-config
Updates mutable fields on the on-chain ProgramConfig. All arguments are optional; only the provided fields are updated. --authority-path must be the current program authority (enforced by has_one = authority on ProgramConfig). Updating --proposed-authority starts a two-step authority handover that must be completed via finalize-proposed-authority.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
update-program-config \
--min-consensus-threshold-bps 6667 \
--vote-duration 86400 \
--tie-breaker-admin <PUBKEY> \
--proposed-authority <PUBKEY> \
--svmgov-program-id <PUBKEY>| Argument | Type | Description |
|---|---|---|
--min-consensus-threshold-bps <u16> | Optional | Minimum stake-weighted consensus threshold in basis points (10000 = 100%). Example: 6000 for 60%. |
--vote-duration <i64> | Optional | Voting window duration, in seconds. Operators have this long after a ballot box is created to cast votes. |
--tie-breaker-admin <Pubkey> | Optional | New tie-breaker admin pubkey (base58). This admin can resolve a stalled ballot box via set-tie-breaker. |
--proposed-authority <Pubkey> | Optional | Proposed new program authority (base58). Becomes active only after finalize-proposed-authority is run by this pubkey. |
--svmgov-program-id <Pubkey> | Optional | New svmgov governance program id (base58) authorized to open ballot boxes. Retargets ncn at a new svmgov deployment without redeploying the ncn program. |
finalize-proposed-authority
Completes a pending two-step authority handover. --authority-path must be the proposed authority previously staged via update-program-config --proposed-authority; the on-chain constraint requires signer == proposed_authority. On success the signer becomes the active program authority and proposed_authority is cleared.
ncn-cli \
--payer-path <PATH_TO_PAYER_KEYPAIR> \
--authority-path <PATH_TO_PROPOSED_AUTHORITY_KEYPAIR> \
--rpc-url <RPC_URL> \
finalize-proposed-authorityupdate-operator-whitelist
Adds or removes operator pubkeys from the on-chain whitelist. Only whitelisted operators are allowed to cast votes. --authority-path must be the current program authority (enforced by has_one = authority on ProgramConfig).
# Add operators
ncn-cli \
--payer-path <PATH_TO_PAYER_KEYPAIR> \
--authority-path <PATH_TO_AUTHORITY_KEYPAIR> \
--rpc-url <RPC_URL> \
update-operator-whitelist \
-a <PUBKEY1>,<PUBKEY2>,<PUBKEY3>
# Remove operators
ncn-cli \
--payer-path <PATH_TO_PAYER_KEYPAIR> \
--authority-path <PATH_TO_AUTHORITY_KEYPAIR> \
--rpc-url https://api.devnet.solana.com \
update-operator-whitelist \
-r PUBKEY_OLD
# Both at once
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
update-operator-whitelist \
-a PUBKEY_NEW \
-r PUBKEY_OLD| Flag | Description |
|---|---|
-a, --add <pubkeys> | Comma-separated operator pubkeys (base58) to add to the whitelist. |
-r, --remove <pubkeys> | Comma-separated operator pubkeys (base58) to remove from the whitelist. |
Voting Commands
cast-vote
Casts an operator vote on a ballot box using an explicit Merkle root and snapshot hash. Prefer cast-vote-from-snapshot when voting from a local snapshot. --authority-path signs as a whitelisted operator; the handler verifies the signer appears in ProgramConfig.whitelisted_operators before recording the vote in the BallotBox.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
cast-vote \
--snapshot-slot 300000000 \
--root <BASE58_ROOT> \
--hash <BASE58_HASH>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to vote in. |
--root <base58> | Yes | Meta merkle tree root, base-58 encoded (32 bytes). |
--hash <base58> | Yes | SHA-256 hash of the MetaMerkleSnapshot file, base-58 encoded (32 bytes). |
cast-vote-from-snapshot
Casts an operator vote using a local MetaMerkleSnapshot file. Reads the root and computes the snapshot hash from --read-path, then submits the vote. --authority-path signs as a whitelisted operator; the handler verifies the signer appears in ProgramConfig.whitelisted_operators before recording the vote in the BallotBox.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
cast-vote-from-snapshot \
--snapshot-slot 300000000 \
--read-path ./meta_merkle-300000000.zip \
--is-compressed true| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to vote in. |
--read-path <PATH> | Yes | Path to a MetaMerkleSnapshot file (compressed .zip or raw) produced by generate-meta-merkle. |
--is-compressed <bool> | No | Set to true when --read-path points to a compressed .zip produced by generate-meta-merkle (default: true). |
remove-vote
Removes the caller’s vote from a BallotBox. Only permitted before consensus is reached and before the vote window expires. --authority-path must be the operator that originally cast the vote; the handler removes only that signer’s tally entry.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
remove-vote --snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to remove the vote from. |
set-tie-breaker
Resolves a stalled ballot by writing an explicit winning ballot. The provided --root / --hash are not required to match any cast ballot. --authority-path must be the tie-breaker admin recorded in ProgramConfig (enforced by has_one = tie_breaker_admin).
# Set tie-breaking result if consensus was not reached
# Note: Can set any ballot value, not limited to existing ballots
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
set-tie-breaker --snapshot-slot <SLOT> \
--root <MERKLE_ROOT> --hash <SNAPSHOT_HASH>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to tie-break. |
--root <base58> | Yes | Tie-breaking meta merkle tree root, base-58 encoded (32 bytes). |
--hash <base58> | Yes | Tie-breaking snapshot hash (SHA-256) of the corresponding MetaMerkleSnapshot, base-58 encoded (32 bytes). |
reset-ballot-box
Resets a bricked BallotBox. Permitted only when the ballot box’s vote window has not yet expired, consensus has not been reached, and ballot tallies are at their maximum capacity. Clears tallies so voting can restart. --authority-path must be the tie-breaker admin recorded in ProgramConfig (enforced by has_one = tie_breaker_admin).
ncn-cli \
--payer-path ~/.config/solana/id.json \
--authority-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
reset-ballot-box --snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to reset. |
finalize-ballot
Finalizes the winning ballot for a snapshot slot. Closes voting for --snapshot-slot once consensus has been reached (or the vote window has expired with a clear winner) and writes the ConsensusResult PDA. The on-chain instruction is permissionless, so --payer-path is used purely as the fee payer for the new PDA and does not need to hold any privileged role. The --authority-path is not used for this command.
ncn-cli \
--payer-path ~/.config/solana/id.json \
--rpc-url https://api.devnet.solana.com \
finalize-ballot --snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to finalize. |
Read / Inspection Commands
Use these commands when you only want to inspect on-chain state (no transactions are sent).
- Use
statusfor a quick all-in-one health check for a slot. - Use
get-*commands for focused machine/operator-readable outputs. - Use
logif you prefer the older generic account lookup flow.
log
Dumps the raw Debug representation of an on-chain account. Selects the account type via --ty; some types require additional arguments such as --snapshot-slot and/or --vote-account.
# View ProgramConfig
ncn-cli \
--rpc-url https://api.devnet.solana.com log \
--ty program-config
# View a BallotBox
ncn-cli \
--rpc-url https://api.devnet.solana.com log \
--ty ballot-box --snapshot-slot <SLOT>
# View a ConsensusResult
ncn-cli \
--rpc-url https://api.devnet.solana.com log \
--ty consensus-result --snapshot-slot <SLOT>
# View a MetaMerkleProof
ncn-cli \
--rpc-url https://api.devnet.solana.com log \
--ty proof \
--snapshot-slot <SLOT> \
--vote-account <VOTE_ACCOUNT_PUBKEY>| Argument | Required | Description |
|---|---|---|
--ty <TYPE> | Yes | Account type to dump: program-config | ballot-box | consensus-result | proof. |
--snapshot-slot <u64> | For ballot/result/proof | Snapshot slot of the ballot box, consensus result, or proof. Required for all --ty except program-config. |
--vote-account <Pubkey> | For proof only | Validator vote account (base58). Required when --ty proof. |
Focused read commands
get-program-config
Prints the on-chain ProgramConfig singleton.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-program-configget-operator-whitelist
Prints the operator whitelist from ProgramConfig.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-operator-whitelistget-ballot
Prints the on-chain BallotBox for a snapshot slot, including tallies and operator votes.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-ballot \
--snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box to fetch. |
get-operator-vote
Prints a single operator’s vote within a ballot box.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-operator-vote \
--snapshot-slot <SLOT> \
--operator <OPERATOR_PUBKEY>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the ballot box. |
--operator <Pubkey> | Yes | Operator pubkey (base58) whose vote should be looked up. |
get-consensus-result
Prints the ConsensusResult PDA for a snapshot slot.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-consensus-result \
--snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the consensus result to fetch. |
get-proof
Prints the MetaMerkleProof PDA for a vote account.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
get-proof \
--snapshot-slot <SLOT> \
--vote-account <VOTE_ACCOUNT_PUBKEY>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot identifying the consensus result the proof belongs to. |
--vote-account <Pubkey> | Yes | Validator vote account (base58) whose proof should be fetched. |
ballot-exists
Checks whether a BallotBox exists for a snapshot slot.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
ballot-exists \
--snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot to check for a BallotBox PDA. |
status
Prints a combined status report: program config, ballot box, and consensus result for a snapshot slot.
ncn-cli \
--rpc-url https://api.devnet.solana.com \
--cluster mainnet \
status \
--snapshot-slot <SLOT>| Argument | Required | Description |
|---|---|---|
--snapshot-slot <u64> | Yes | Snapshot slot to summarize. |
Typical Operator Workflow
# 1. Wait for target slot and generate snapshot
ncn-cli \
await-snapshot \
--scan-interval <INTERVAL_IN_MINUTES> \
--slot <SLOT> \
--snapshots-dir /mnt/ledger/snapshots \
--backup-snapshots-dir /mnt/ledger/gov-ledger-backup/snapshots \
--backup-ledger-dir /mnt/ledger/gov-ledger-backup \
--agave-ledger-tool-path /home/sol/.local/share/solana/install/active_release/bin/agave-ledger-tool \
--ledger-path /mnt/ledger \
--generate-meta-merkle
# 2. (Optional) Verify the snapshot looks correct
ncn-cli \
--authority-path <PATH_TO_AUTHORITY_KEYPAIR> \
log-meta-merkle-hash \
--read-path <PATH_TO_META_MERKLE_FILE> \
--is-compressed true
# 3. Upload to verifier service (via HTTP POST /upload)
# 4. Cast vote on-chain
ncn-cli \
--payer-path <PATH_TO_PAYER_KEYPAIR> \
--authority-path <PATH_TO_AUTHORITY_KEYPAIR> \
--rpc-url https://api.mainnet-beta.solana.com \
cast-vote-from-snapshot \
--snapshot-slot <SLOT> \
--read-path <PATH_TO_META_MERKLE_FILE> \
--is-compressed true
# 5. Check consensus
ncn-cli \
--rpc-url <RPC_URL> log \
--ty ballot-box --snapshot-slot <SLOT>
# 6. Finalize once consensus is reached (permissionless; --authority-path is not used)
ncn-cli \
--payer-path <PATH_TO_PAYER_KEYPAIR> \
--rpc-url <RPC_URL> \
finalize-ballot --snapshot-slot <SLOT>