Skip to Content
NCNncn-snapshot Program

ncn-snapshot Program

Program ID: ncnwF8AgynRcdEnGLcprSQNaKvgSMTgk3yPRc8cf9Zf

The ncn-snapshot Solana program is the on-chain component of NCN. It manages the operator whitelist, collects snapshot votes into BallotBox accounts, determines consensus, and publishes ConsensusResult accounts that the svmgov program uses for stake proof verification.


Account Structures

ProgramConfig

PDA seeds: ["ProgramConfig"]

The global singleton configuration. Initialized once by the governance authority.

FieldTypeDescription
authorityPubkeyAdmin who can update this config and manage whitelist
proposed_authorityOption<Pubkey>Pending new authority (two-step transfer)
whitelisted_operatorsVec<Pubkey> (max 64)Operators eligible to cast votes
min_consensus_threshold_bpsu16Min fraction of operators required for consensus (basis points, 1–10,000)
tie_breaker_adminPubkeyCan resolve deadlocks after vote expiry
vote_durationi64Seconds a BallotBox is open for voting
svmgov_program_pubkeyPubkeysvmgov program whose Proposal PDAs are authorized to open ballot boxes

Constant: MAX_OPERATOR_WHITELIST = 64


BallotBox

PDA seeds: ["BallotBox", snapshot_slot.to_le_bytes()]

One per governance proposal cycle. Tracks all operator votes for a given snapshot_slot.

FieldTypeDescription
bumpu8PDA bump
epochu64Epoch at creation
slot_createdu64Slot at creation
slot_consensus_reachedu64Slot when consensus was first reached (0 = not yet)
min_consensus_threshold_bpsu16Threshold snapshot at creation time
winning_ballotBallotThe ballot that reached consensus
operator_votesVec<OperatorVote> (max 64)One entry per operator vote cast
ballot_talliesVec<BallotTally> (max 64)Running count per unique ballot
vote_expiry_timestampi64Unix timestamp when voting closes
snapshot_slotu64The target slot for this snapshot cycle
voter_listVec<Pubkey> (max 64)Snapshot of whitelist at creation
tie_breaker_consensusbooltrue if resolved by tie-breaker admin

Inner types:

pub struct Ballot { pub meta_merkle_root: [u8; 32], pub snapshot_hash: [u8; 32], } pub struct OperatorVote { pub operator: Pubkey, pub slot_voted: u64, pub ballot_index: u8, // index into ballot_tallies } pub struct BallotTally { pub index: u8, pub ballot: Ballot, pub tally: u8, // number of operators who voted this ballot }

ConsensusResult

PDA seeds: ["ConsensusResult", snapshot_slot.to_le_bytes()]

The final, immutable output. Created by finalize_ballot after consensus is reached.

FieldTypeDescription
snapshot_slotu64The slot this result is for
ballotBallotThe winning ballot (root + hash)
tie_breaker_consensusboolWhether resolved by tie-breaker

MetaMerkleProof

PDA seeds: ["MetaMerkleProof", consensus_result_pubkey, vote_account_pubkey]

Temporary storage for a validator’s proof data. Variable-size account.

FieldTypeDescription
payerPubkeyCreator who can close at any time
consensus_resultPubkeyThe ConsensusResult this proof belongs to
meta_merkle_leafMetaMerkleLeafLeaf data (voting_wallet, vote_account, stake_root, active_stake)
meta_merkle_proofVec<[u8; 32]>Sibling hashes from leaf to root
close_timestampi64After this time, anyone can close the account

Instructions

init_program_config

Initializes the ProgramConfig singleton. Called once at program deployment.

Signers: payer, authority

Accounts: program_config (init, PDA)
ParameterTypeDescription
svmgov_program_pubkeyPubkeysvmgov program authorized to open ballot boxes

update_program_config

Updates any subset of config fields. All parameters are optional.

Signer: authority

ParameterTypeValidation
proposed_authorityOption<Pubkey>
min_consensus_threshold_bpsOption<u16>Must be > 0 and ≤ 10,000
tie_breaker_adminOption<Pubkey>
vote_durationOption<i64>Must be > 0
svmgov_program_pubkeyOption<Pubkey>Retargets which svmgov program can open ballot boxes

finalize_proposed_authority

Completes a two-step authority transfer. The new authority signs this instruction.

Signer: new authority (must match program_config.proposed_authority)

Clears proposed_authority after accepting.


update_operator_whitelist

Add or remove operators atomically.

Signer: authority

ParameterValidation
operators_to_add: Option<Vec<Pubkey>>Deduplicates; enforces max 64 total
operators_to_remove: Option<Vec<Pubkey>>

Add and remove lists must not overlap. If the same pubkey appears in both, the instruction fails with OverlappingWhitelistEntries.


init_ballot_box

Creates a new BallotBox for a given snapshot_slot. Called by the svmgov program via CPI when a governance proposal reaches the support threshold.

Signers: payer, proposal (governance proposal PDA, acts as CPI signer)

ParameterValidation
snapshot_slot: u64Must be > current_slot
proposal_seed: u64Seed used to verify the CPI signer is a valid governance proposal PDA
spl_vote_account: PubkeyProposer’s vote account

Snapshots whitelisted_operators into voter_list at creation time.

The proposal account must be a PDA from the svmgov program stored in ProgramConfig.svmgov_program_pubkey (govYkyQ3ePtGULAtY6V75qjWE8UH4vCUVQ1W4HdCAZU in the current release manifest). Compile with skip-pda-check feature to disable this check for testing.


cast_vote

Operator casts a vote on a BallotBox.

Signer: operator (must be in voter_list)

ParameterValidation
ballot: Ballotmeta_merkle_root must not be all zeros

Checks: operator in voter list, voting not expired, operator hasn’t already voted, ballot_tallies not full. Automatically sets winning_ballot when the threshold is crossed for the first time.


remove_vote

Removes an operator’s vote. Only allowed before expiry and before consensus.

Signer: operator

Decrements the tally but keeps the BallotTally entry (to preserve ballot indices for other operators who voted the same ballot).


set_tie_breaker

Allows the tie_breaker_admin to force a winner after voting expires without consensus.

Signer: tie_breaker_admin (from ProgramConfig)

ParameterValidation
ballot: BallotAny ballot value (can be any valid root+hash)

Allowed only when: voting has expired AND consensus has not been reached. Sets tie_breaker_consensus = true.


reset_ballot_box

Emergency recovery for a BallotBox that is stuck because all 64 tally slots are full of different ballots (no consensus possible without this reset).

Signer: tie_breaker_admin

Allowed only when: no consensus, not expired, and ballot_tallies.len() == MAX_BALLOT_TALLIES. Clears all operator_votes and ballot_tallies.


finalize_ballot

Permissionless. Creates the ConsensusResult PDA from the BallotBox.winning_ballot. Anyone can call this once consensus is reached.

Accounts: ballot_box (read), consensus_result (init)


init_meta_merkle_proof

Stores a validator’s proof data in a temporary PDA. Immediately verifies the proof against the ConsensusResult.

Signer: payer (pays rent, can close at any time)

ParameterDescription
meta_merkle_leaf: MetaMerkleLeafLeaf data
meta_merkle_proof: Vec<[u8;32]>Proof path
close_timestamp: i64When others can close this PDA

verify_merkle_proof

Permissionless CPI-able instruction. Verifies that a vote account (and optionally a stake account) is in the ConsensusResult.

ParameterDescription
stake_merkle_proof: Option<Vec<[u8;32]>>Stake level proof (required with stake leaf)
stake_merkle_leaf: Option<StakeMerkleLeaf>Stake account leaf data

If stake arguments are provided, also verifies the stake leaf is in meta_merkle_leaf.stake_merkle_root. Both must be provided together or neither.


close_meta_merkle_proof

Closes a MetaMerkleProof PDA to reclaim rent.

  • Creator can close at any time
  • Anyone can close after close_timestamp

Error Codes

ErrorDescription
OperatorNotWhitelistedSigner is not in the voter list
OperatorHasVotedOperator already cast a vote
OperatorHasNotVotedTried to remove a vote that doesn’t exist
VotingExpiredVoting window has closed
VotingNotExpiredTie-breaker called before expiry
ConsensusReachedAction not allowed after consensus (e.g., remove vote)
ConsensusNotReachedTried to finalize before consensus
InvalidBallotBallot has all-zero root
InvalidMerkleInputsStake proof args must both be provided or both absent
InvalidMerkleProofProof verification failed
VecFullBallot tallies at max capacity
OverlappingWhitelistEntriesSame pubkey in both add and remove lists
InvalidBallotIndexInternal ballot index inconsistency
InvalidSnapshotSlotSnapshot slot must be greater than current slot
BallotTalliesNotMaxLengthReset attempted when tallies not full
InvalidProposalCPI caller is not a valid governance proposal PDA
Last updated on