NCN Implementation Guide
Introduction
Node Consensus Networks (NCN) are robust consensus networks built on Solana. They enables network participants to leverage staked assets to agree on critical network decisions. NCNs utilize Jito’s restaking infrastructure, allowing operators with delegated tokens to vote on network parameters and states.
This tutorial focuses on a pre-built NCN program that serves as a template or base that you can use to create your own NCN program. To help you understand how it works, we will walk through building a simulation test that covers the majority of its setup and functionality. We do not recommend most NCN developers build an NCN from scratch. Rather, we suggest using this prebuilt program as a starting point and customizing it according to your needs.
By following the simulation test setup in this guide, you will gain hands-on experience with the entire NCN lifecycle: initializing vaults and operators using Jito’s restaking and vault programs, configuring the NCN program, and executing the full voting process.
The purpose of NCNs
Decentralized networks require reliable mechanisms for participants to reach consensus without central authorities. The NCN addresses this need by:
- Providing a secure voting framework where influence is proportional to the amount of stake held.
- Supporting multiple token types with configurable voting weights, allowing flexibility in how voting power is assigned.
- Creating verifiable and immutable records of consensus decisions on the blockchain.
- Establishing a solid foundation for network governance and parameter setting.
NCN components
To run an NCN, you need one or more of each of the following three components, which interact with each other: Vaults, Operators, and the NCN Program itself.
1. Vaults
Vaults are accounts that hold tokens and delegate them to operators. They play a crucial role in the NCN by:
- Holding the tokens used for staking.
- Delegating stake (voting power) to chosen operators.
- Enabling stake-weighted participation in the network’s governance.
2. Operators
Operators are accounts that receive delegated stake from vaults and actively participate in the voting process. Their key functions are:
- Receiving stake delegations from one or more vaults.
- Casting votes on behalf of the delegated stake during consensus rounds.
- Forming the network of active participants who drive the consensus process.
3. Keepers
Keepers are offchain agents that monitor the network and submit onchain instructions to advance the NCN through its lifecycle. They operate autonomously and are fully permissionless. Their responsibilities include:
- Monitoring the current onchain state.
- Executing program instructions to progress through state like voting, post-vote logging and epoch finalization.
- Emitting metrics or logs to external systems for observability.
4. NCN program
The NCN Program is the core onchain component of the system. It’s the smart contract that NCN developers build and deploy. Its main responsibilities are:
- Storing the global configuration parameters for the NCN instance.
- Maintaining the registry of participating vaults and supported token types.
- Managing the state for each voting epoch (consensus cycle).
NCN Lifecycle
The Node Consensus Network operates in a well-defined lifecycle that consists of three main phases:
-
Initial Setup (One-time): This phase involves establishing the foundational infrastructure of the NCN. It includes:
- Configuring the NCN parameters
- Initializing the vault registry
-
Registering supported token types and assigning weights
The initial setup is performed only once when the NCN is first deployed, with occasional administrative updates as needed (such as adjusting token weights or adding new supported tokens).
-
Snapshotting (Recurring): At the beginning of each consensus cycle (epoch), the system captures the current state of all participants:
- Creating epoch state and weight tables
- Taking snapshots of operator stake weights
- Recording vault-operator delegations
-
Calculating total voting power distribution
This phase ensures that voting is based on a consistent, point-in-time view of the network, preventing manipulation during the voting process.
-
Voting (Recurring): After snapshotting is complete, operators can cast their votes:
- Operators submit their choices (e.g., weather status)
- Votes are weighted according to the operator’s stake
Get to know the program template
Our example NCN Program facilitates consensus on a simple “weather status” using a stake-weighted voting mechanism. It operates in distinct time periods called epochs (your NCN’s epochs do not have to be equivalent to a Solana epoch). The program uses a weight-based system to determine the influence (voting power) of different operators. Consensus is achieved when votes representing at least 66% of the total participating stake weight agree on the same outcome (ballot).
Key components
The program uses several types of accounts:
- Global Accounts: Initialized once at the start and updated infrequently.
Config
: Stores global settings like epoch timing parameters (epochs_before_stall
,epochs_after_consensus_before_close
) and voting validity periods (valid_slots_after_consensus
).VaultRegistry
: Manages the list of registered vaults and the different types of stake tokens (mints) the NCN supports.AccountPayer
: An empty PDA account used to hold SOL temporarily for paying rent during account creation or reallocation.
- Per-Consensus Cycle Accounts: Initialized at the beginning of each epoch and usually closed shortly after the cycle ends.
WeightTable
: Stores the specific voting weights assigned to different stake tokens for the current epoch.EpochState
: Tracks the status and progress of the current epoch’s consensus cycle.BallotBox
: Handles the collection and stake-weighted tallying of votes for the current epoch’s decision (e.g., weather status).EpochSnapshot
: Captures the state of stake delegations at the beginning of the epoch to ensure consistent voting weights throughout the cycle.OperatorSnapshot
: Records each operator’s total stake weight and delegation breakdown for the current epoch.ConsensusResult
: Stores the final outcome (the winning ballot and associated details) for the completed epoch.EpochMarker
: A marker account created when all temporary accounts for an epoch have been successfully closed.
- Component Structures: These are not separate accounts but important data structures used within the accounts above.
Ballot
: Represents a single potential outcome in the consensus process.BallotTally
: Aggregates votes and stake weight for a specific ballot.OperatorVote
: Records a vote cast by a single operator.VaultOperatorStakeWeight
: Tracks the weighted stake from a specific vault to an operator.StMintEntry
: Represents a supported token mint and its voting weight in the VaultRegistry.VaultEntry
: Represents a registered vault in the VaultRegistry.
Weather status system
The goal of the NCN program is to come to consensus on the weather in Solana Beach. For the purposes of keeping this tutorial simple, our weather statuses are as follows:
- Sunny (0): Represents clear, sunny weather.
- Cloudy (1): Represents cloudy weather conditions.
- Rainy (2): Represents rainy weather conditions.
Operators vote on these status values. The program tallies the votes, weighting each vote by the operator’s associated stake weight, to determine the final consensus result. Leveraging the final result of this NCN, we can build onchain programs whose behavior is dependent on the weather in Solana Beach.
Consensus mechanism
The consensus process follows these steps:
- Operators cast votes, choosing a specific weather status (Sunny, Cloudy, or Rainy).
- Each vote’s influence is determined by the operator’s total stake weight, calculated from delegations received.
- Votes are collected and tallied within the
BallotBox
account for the current epoch. - Consensus is reached when one weather status receives votes representing ≥66% of the total stake weight participating in that epoch.
- The final consensus result (winning status, total weight supporting it, etc.) is recorded in the
ConsensusResult
account.
Onchain program overview
The onchain program is written in Rust (without using the Anchor framework) and consists of several instructions that can be called to perform various actions within the NCN. The instruction logic resides in the /program
directory, while shared core logic is located in the /core
directory.
The instructions are broadly categorized:
- Admin Instructions: These require administrator privileges and are used for initial setup and configuration.
admin_initialize_config
: Initializes the mainConfig
account.admin_register_st_mint
: Registers a new type of stake token (ST) the NCN will support.admin_set_new_admin
: Transfers administrative control to a new keypair.admin_set_parameters
: Updates parameters within theConfig
account.admin_set_st_mint
: Updates details for an existing supported token mint (Deprecated/Redundant? Checkadmin_register_st_mint
andadmin_set_weight
).admin_set_tie_breaker
: Configures the tie-breaking mechanism or authority.admin_set_weight
: Sets or updates the voting weight for a specific supported token mint.
- Permissionless Keeper Instructions: These are permissionless instructions, meaning anyone can call them to advance the state of the NCN, typically moving between epoch phases. They ensure the NCN progresses correctly.
initialize_epoch_state
: Creates theEpochState
account for a new epoch.initialize_vault_registry
: Creates the initialVaultRegistry
account.realloc_vault_registry
: Increases the size of theVaultRegistry
account, to reach the desired size. Solana has a limitation when it comes to the size of the account that you can allocate in one call, so when you have a larger account, you will need to call realloc on it multiple times to reach the desired size.initialize_weight_table
: Creates theWeightTable
account for an epoch.realloc_weight_table
: Increases the size of theWeightTable
account.initialize_epoch_snapshot
: Creates the mainEpochSnapshot
account.initialize_operator_snapshot
: Creates anOperatorSnapshot
account for a specific operator within an epoch.set_epoch_weights
: Populates theWeightTable
with weights from theVaultRegistry
.snapshot_vault_operator_delegation
: Records the weighted stake from a specific vault delegation into the relevantOperatorSnapshot
.initialize_ballot_box
: Creates theBallotBox
account for voting in an epoch.realloc_ballot_box
: Increases the size of theBallotBox
account.register_vault
: Registers a vault (that has already been approved via Jito handshake) with the NCN program’sVaultRegistry
.close_epoch_account
: Closes temporary epoch-specific accounts (likeEpochState
,BallotBox
, etc.) after they are no longer needed, reclaiming rent.
- Operator Instruction: This is the primary action taken by participants during a consensus cycle.
cast_vote
: Allows an operator (using their admin key) to submit their vote for the current epoch.
For more details, you can always check the source code or the API documentation here.
Build and run the simulation test
This section will walk through building a simulation test of our example NCN program. The test represents a comprehensive scenario designed to mimic a complete NCN system. It involves multiple operators, vaults, and different types of tokens. The test covers the entire workflow, from the initial setup of participants and the NCN program itself, through the voting process, and finally to reaching and verifying consensus. It heavily utilizes Jito’s restaking and vault infrastructure alongside the custom NCN voting logic.
The NCN program used can be found here. By creating a simulation test of this NCN, you’ll be better prepared to use it as a template or base that you can adapt to create your own NCN program. Just a reminder: we do not recommend most NCN developers build their NCN from scratch. Rather, we suggest using this prebuilt program as a starting point and customizing it according to your needs.
The simulation test we’ll be creating below can also be found in the example NCN repository. However, you’ll understand the system better if you write the test along with us, so feel free to clone the repository, delete the test file ./integration_tests/test/ncn_program/simulation_test.rs
, and follow along. This will give you hands-on experience with the entire NCN lifecycle: initializing vaults and operators using Jito’s restaking and vault programs, configuring the NCN program, and executing the full voting process.
Prerequisites
Before running the simulation test, ensure you have completed the following setup steps:
- Build the NCN onchain program using Cargo:
cargo build-sbf --manifest-path program/Cargo.toml --sbf-out-dir integration_tests/tests/fixtures
- Ensure you have the correct versions installed:
- Solana CLI: 2.2.6 (recommended)
- Rust/Cargo: 1.81 or newer
Building the Simulation Test
Let’s build the simulation test step by step.
1. Create a new file
You can start with a blank file. Create a new file named simulation_test.rs
inside the integration_tests/tests
folder. Copy and paste the following boilerplate code at the bottom of your test function:
#[cfg(test)]
mod tests {
use crate::fixtures::{test_builder::TestBuilder, TestResult};
use jito_restaking_core::{config::Config, ncn_vault_ticket::NcnVaultTicket};
use ncn_program_core::{ballot_box::WeatherStatus, constants::WEIGHT};
use solana_sdk::{msg, signature::Keypair, signer::Signer};
#[tokio::test]
async fn simulation_test() -> TestResult<()> {
// YOUR TEST CODE WILL GO HERE
// 2. ENVIRONMENT SETUP
// 3. NCN SETUP
// 4. OPERATORS AND VAULTS SETUP
// 5. NCN PROGRAM CONFIGURATION
// 6. Epoch Snapshot and Voting Preparation
// 7. VOTING
// 8. REWARDS DISTRIBUTION
// 9. VERIFICATION
// 10. CLEANUP
Ok(())
}
}
Unless otherwise specified, all of the code snippets provided in this guide represent code that should go inside the simulation_test
test function, in the order provided.
Next, you need to make this new test discoverable. Copy and paste the following line into the integration_tests/tests/mod.rs
file to declare the new module:
// integration_tests/tests/mod.rs
mod simulation_test;
Now, you can run this specific test using the following command:
SBF_OUT_DIR=integration_tests/tests/fixtures cargo test -p ncn-program-integration-tests --test tests simulation_test
This command targets the ncn-program-integration-tests
package and runs only the simulation_test
test function. If you want to run all tests in the suite, simply remove the test name filter (-p ncn-program-integration-tests --test tests simulation_test
) from the command.
Currently, the test will pass because it doesn’t contain any logic yet. You should see output similar to this:
running 1 test
test ncn_program::simulation_test::tests::simulation_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 54 filtered out; finished in 0.00s
2. Environment Setup
The first step within our test function is to set up the testing environment using the TestBuilder
. Copy and paste the following code at the bottom of your test function:
let mut fixture = TestBuilder::new().await;
The TestBuilder
is a test utility that encapsulates and simplifies the setup process for NCN program testing. It provides:
- A local test validator environment with pre-loaded programs
- Clients for interacting with the NCN, Vault, and Restaking programs
- Helper methods for common operations (creating operators, vaults, advancing clock time)
- Management of test accounts, keypairs, and token mints
This and other utility functions (like add_operators_to_test_ncn
, add_vaults_to_test_ncn
) abstract away much of the complex, repetitive setup code, allowing tests to focus on the specific behaviors being verified rather than boilerplate infrastructure.
Since we are running this test locally against a test ledger, we need to initialize the Jito Restaking and Vault programs on the ledger. In a real network environment (devnet, mainnet), these programs would already be deployed and configured.
Copy and paste the following code at the bottom of your test function:
fixture.initialize_restaking_and_vault_programs().await?;
Finally, let’s prepare some client objects and configuration variables we’ll use throughout the test.
Copy and paste the following code at the bottom of your test function:
let ncn_program_client = fixture.ncn_program_client();
let vault_program_client = fixture.vault_client();
let restaking_client = fixture.restaking_program_client();
// Define test parameters
const OPERATOR_COUNT: usize = 13; // Number of operators to simulate
let mints = vec![
(Keypair::new(), WEIGHT), // Alice: Base weight
(Keypair::new(), WEIGHT * 2), // Bob: Double weight
(Keypair::new(), WEIGHT * 3), // Charlie: Triple weight
(Keypair::new(), WEIGHT * 4), // Dave: Quadruple weight
];
let delegations = [
1, // Minimum delegation amount (e.g., 1 lamport)
10_000_000_000, // 10 tokens (assuming 9 decimals)
100_000_000_000, // 100 tokens
1_000_000_000_000, // 1,000 tokens
10_000_000_000_000, // 10,000 tokens
];
This code does the following:
- Gets client handles for interacting with the NCN, Vault, and Restaking programs.
- Defines
OPERATOR_COUNT
to specify how many operators we’ll create. - Sets up
mints
: a list of keypairs representing different SPL token mints and their corresponding voting weights. We use different weights to test the stake-weighting mechanism.WEIGHT
is likely a constant representing the base unit of weight. - Defines
delegations
: an array of different token amounts (in lamports, assuming 9 decimals for typical SPL tokens) that vaults will delegate to operators.
3. NCN Setup
Now, let’s create the NCN account using the Jito Restaking program. The create_test_ncn
helper function handles the necessary instruction calls.
Copy and paste the following code at the bottom of your test function:
let mut test_ncn = fixture.create_test_ncn().await?;
let ncn_pubkey = test_ncn.ncn_root.ncn_pubkey;
This step:
- Calls the Jito Restaking program to create a new Node Consensus Network (NCN) account and its associated administrative structures.
- Stores the public key (
ncn_pubkey
) of the newly created NCN, which we’ll need to interact with it later.
If you run the test at this point (cargo test ... simulation_test
), you should see transaction logs in the output, indicating that the NCN creation instructions were executed successfully.
4. Operators and Vaults Setup
This phase is crucial for simulating a realistic network. We will create the operators who vote and the vaults that provide the stake (voting power).
4.1 Operator Creation and NCN Connection
We’ll add the specified number of operators (OPERATOR_COUNT
) to our NCN using another helper function.
Copy and paste the following code at the bottom of your test function:
fixture
.add_operators_to_test_ncn(&mut test_ncn, OPERATOR_COUNT, Some(100))
.await?;
This add_operators_to_test_ncn
function performs several actions by calling instructions in the Jito Restaking program:
- Creates
OPERATOR_COUNT
(13 in our case) separate operator accounts. - Sets an optional operator fee (here, 100 basis points = 1%).
- Establishes a secure, bidirectional “handshake” between each newly created operator and the NCN.
The handshake process involves multiple steps:
- Creating the operator account itself, managed by its unique admin keypair.
- Initializing the state that tracks the relationship between the NCN and the operator (
do_initialize_ncn_operator_state
). - Warming up the connection from the NCN’s perspective (
do_ncn_warmup_operator
). - Warming up the connection from the operator’s perspective (
do_operator_warmup_ncn
).
For more information on this, please read the guide here
This handshake is essential for security. It ensures that operators must explicitly connect to the NCN (and vice-versa) and potentially wait through an activation period before they can participate in voting.
4.2 Vault Creation
Next, we create vaults to hold the different types of tokens we defined earlier. We’ll distribute them across the token types. Note that you can have more than one vault with the same ST Mint (Support Token Mint).
Copy and paste the following code at the bottom of your test function:
// Create vaults associated with different token mints
{
// Create 3 vaults for Alice (base weight)
fixture
.add_vaults_to_test_ncn(&mut test_ncn, 3, Some(mints[0].0.insecure_clone()))
.await?;
// Create 2 vaults for Bob (double weight)
fixture
.add_vaults_to_test_ncn(&mut test_ncn, 2, Some(mints[1].0.insecure_clone()))
.await?;
// Create 1 vault for Charlie (triple weight)
fixture
.add_vaults_to_test_ncn(&mut test_ncn, 1, Some(mints[2].0.insecure_clone()))
.await?;
// Create 1 vault for Dave (quadruple weight)
fixture
.add_vaults_to_test_ncn(&mut test_ncn, 1, Some(mints[3].0.insecure_clone()))
.await?;
}
The add_vaults_to_test_ncn
helper function orchestrates calls to both the Jito Vault and Jito Restaking programs to:
- Create a total of 7 vaults (3 + 2 + 1 + 1).
- Associate each group of vaults with one of our predefined token mints (
mints[0]
,mints[1]
, etc.). - Initialize the vault accounts using the Jito Vault program (setting zero fees, which is common for testing).
- Mint tokens for the vaults if needed (though here we provide the mints).
- Establish bidirectional handshakes “Tickets” between each vault and the NCN using specific Jito Restaking instructions (
do_initialize_ncn_vault_ticket
,do_warmup_ncn_vault_ticket
). - Establish corresponding handshakes “Tickets” using Jito Vault program instructions (
do_initialize_vault_ncn_ticket
,do_warmup_vault_ncn_ticket
). - Establish bidirectional handshakes “Tickets” between each new vault and all existing operators using Jito Restaking (
do_initialize_operator_vault_ticket
,do_warmup_operator_vault_ticket
) and Jito Vault (do_initialize_vault_operator_delegation
) instructions. Note thatdo_initialize_vault_operator_delegation
only sets up the potential for delegation; no actual tokens are delegated yet. - Advance the simulated clock (
fixture.advance_slots
) after handshakes “Tickets” to ensure the relationships become active, simulating the necessary waiting period.
Creating vaults with different token types allows us to test how the NCN handles varying voting power based on token weights.
4.3 Delegation Setup
This is where vaults actually delegate their tokens (stake) to operators, granting them voting power. We’ll iterate through operators and vaults to create delegations.
Copy and paste the following code at the bottom of your test function:
// Vaults delegate stake to operators
{
// Iterate through all operators except the last one
for (index, operator_root) in test_ncn
.operators
.iter()
.take(OPERATOR_COUNT - 1)
.enumerate()
{
// Each vault delegates to this operator
for vault_root in test_ncn.vaults.iter() {
// Cycle through the predefined delegation amounts
let delegation_amount = delegations[index % delegations.len()];
if delegation_amount > 0 {
// Call the Vault program to add the delegation
vault_program_client
.do_add_delegation(
vault_root, // The vault delegating
&operator_root.operator_pubkey, // The operator receiving
delegation_amount, // The amount to delegate
)
.await
.unwrap();
}
}
}
}
The delegation process is where voting power is established. Each vault delegates tokens to operators, which determines:
- How much voting power each operator has
- How token weights multiply that power
- The distribution of influence across the network
Key aspects of the delegation setup:
-
Every vault delegates to every operator (except the last one for this example)
-
Note that vaults can choose whom to delegate to, they don’t have to delegate to all operators
- Delegation amounts cycle through the
delegations
array to test different scenarios - The last operator intentionally receives zero delegation to test the system’s handling of operators without stake
- The delegation is performed directly through the vault program using
do_add_delegation
which will call a specific instruction in the vault program to do that
Each operator accumulates voting power from all the different delegations they receive. The total voting power for an operator is the sum of the weighted values of each delegation.
Example:
- Vault A (holding Alice, weight W) delegates 100 tokens to Operator X. Power contribution: 100 * W.
- Vault B (holding Bob, weight 2W) delegates 50 tokens to Operator X. Power contribution: 50 2W = 100 W.
- Operator X’s total voting power would be (100 W) + (50 2W) = 200 * W.
This distributed delegation model enables testing complex scenarios where:
- Operators have vastly different amounts of influence.
- Tokens with higher weights contribute disproportionately more voting power.
- The distribution of delegations affects consensus outcomes.
The deliberate omission of delegation to the last operator creates a control case to verify that operators with zero stake cannot influence the voting process, which is a critical security feature.
You can run the test now and see the output.
4.4 Delegation Architecture and Voting Power Calculation
The delegation architecture follows a multiplication relationship:
- Operator Voting Power = Sum of (Delegation Amount × Delegated Token’s Weight)
Each operator accumulates voting power from all the different delegations they receive. The total voting power for an operator is the sum of the weighted values of each delegation.
Example:
- Vault A (holding TKN1, weight W) delegates 100 tokens to Operator X. Power contribution: 100 * W.
- Vault B (holding TKN2, weight 2W) delegates 50 tokens to Operator X. Power contribution: 50 2W = 100 W.
- Operator X’s total voting power would be (100 W) + (50 2W) = 200 * W.
This distributed delegation model enables testing complex scenarios where:
- Operators have vastly different amounts of influence.
- Tokens with higher weights contribute disproportionately more voting power.
- The distribution of delegations affects consensus outcomes.
The deliberate omission of delegation to the last operator creates a control case to verify that operators with zero stake cannot influence the voting process, which is a critical security feature.
You can run the test now and see the output.
5. NCN Program Configuration
Until now, all the code we’ve written uses the Jito restaking program and Jito vault program. Now we will start using the example NCN program that you will have to deploy.
The NCN Program Configuration phase establishes the on-chain infrastructure necessary for the voting and consensus mechanisms. This includes setting up configuration parameters, creating data structures, and registering the token types and vaults that will participate in the system.
5.1 Program Configuration Initialization
First, we initialize the main configuration account for our NCN instance.
Copy and paste the following code at the bottom of your test function:
// Initialize the main Config account for the NCN program
ncn_program_client
.do_initialize_config(test_ncn.ncn_root.ncn_pubkey, &test_ncn.ncn_root.ncn_admin)
.await?;
This step initializes the core configuration for the NCN program with critical parameters:
- NCN Admin: The authority that can modify configuration settings, this admin has to be the same admin for the NCN account from Jito restaking program side.
- Epochs Before Stall: How many epochs before a non-completed consensus cycle is considered stalled (default: 3)
- Epochs After Consensus Before Close: How long to wait after consensus before closing epoch data (default: 10)
- Valid Slots After Consensus: How many slots votes are still accepted after consensus is reached (default: 10000)
Under the hood, this creates an NcnConfig
account that stores these parameters and serves as the authoritative configuration for this NCN instance.
5.2 Vault Registry Initialization
The vault registry account is a large one, so it is not possible to initialize it in one call due to Solana network limitations. We will have to call the NCN program multiple times to get to the full size. The first call will be an init call to the instruction admin_initialize_vault_registry
. After that, we will call a realloc instruction admin_realloc_vault_registry
to increase the size of the account. This will be done in a loop until the account is the correct size.
The realloc will take care of assigning the default values to the vault registry account once the desirable size is reached. In our example, we will do that by calling one function do_full_initialize_vault_registry
. If you want to learn more about this, you can check the source code.
Copy and paste the following code at the bottom of your test function:
// Initialize the VaultRegistry account (handles potential reallocations)
ncn_program_client
.do_full_initialize_vault_registry(test_ncn.ncn_root.ncn_pubkey)
.await?;
The vault registry is a critical data structure that:
- Tracks all supported vault accounts
- Maintains the list of supported token mints (token types)
- Records the weight assigned to each token type
- Serves as the source of truth for vault and token configurations
Note that this is only initializing the vault registry. The vaults and the supported tokens will be registered in the next steps.
Check out the vault registry struct here
5.3 Activating Relationships with Time Advancement
Next, we advance the simulation clock to ensure that all previously established handshake relationships (NCN-Operator, NCN-Vault, Operator-Vault) become active, as Jito’s restaking infrastructure often includes activation periods.
Copy and paste the following code at the bottom of your test function:
// Fast-forward time to simulate a full epoch passing
// This is needed for all the relationships to get activated
let restaking_config_address =
Config::find_program_address(&jito_restaking_program::id()).0;
let restaking_config = restaking_client
.get_config(&restaking_config_address)
.await?;
let epoch_length = restaking_config.epoch_length();
fixture
.warp_slot_incremental(epoch_length * 2)
.await
.unwrap();
This section:
- Retrieves the epoch length from the restaking program configuration
- Advances the simulation time by two full epochs
- Ensures all handshake relationships between NCN, operators, and vaults become active
The time advancement is necessary because Jito’s restaking infrastructure uses an activation period for security. This prevents malicious actors from quickly creating and voting with fake operators or vaults by enforcing a waiting period before they can participate.
Now it is time to register the supported tokens with the NCN program and assign weights to each mint for voting power calculations.
Copy and paste the following code at the bottom of your test function:
// Register each Supported Token (ST) mint and its weight in the NCN's VaultRegistry
for (mint, weight) in mints.iter() {
ncn_program_client
.do_admin_register_st_mint(ncn_pubkey, mint.pubkey(), *weight)
.await?;
}
This step registers each Supported Token (ST) mint with the NCN program and assigns the appropriate weight:
- Each token mint (Alice, Bob, Charlie, Dave) is registered with its corresponding weight
- The weights determine the voting power multiplier for delegations in that token
- Only the NCN admin has the authority to register tokens, ensuring trust in the system
- Registration involves updating the vault registry with each token’s data
- The NCN admin can update the weights of the tokens at any time, which will affect the voting power of the delegations in the next consensus cycle
The weight assignment is fundamental to the design, allowing different tokens to have varying influence on the voting process based on their economic significance or other criteria determined by the NCN administrators.
It’s good to know that in real-life examples, NCNs will probably want to set the token weights based on the token’s price or market cap. To do so, you will have to use an oracle to get the price of the token and then set the weight based on that. In this case, you will have to store the feed of the price in this step instead of the weight.
5.5 Vault Registration
Registering a vault is a permissionless operation. The reason is the admin has already given permission to the vault to be part of the NCN in the vault registration step earlier, so this step is just to register the vault in the NCN program.
Copy and paste the following code at the bottom of your test function:
// Register all the vaults in the ncn program
for vault in test_ncn.vaults.iter() {
let vault = vault.vault_pubkey;
let (ncn_vault_ticket, _, _) = NcnVaultTicket::find_program_address(
&jito_restaking_program::id(),
&ncn_pubkey,
&vault,
);
ncn_program_client
.do_register_vault(ncn_pubkey, vault, ncn_vault_ticket)
.await?;
}
The final configuration step registers each vault with the NCN program:
- For each vault created earlier, the system finds its NCN vault ticket PDA (Program Derived Address)
- The vault is registered in the NCN program’s vault registry
- This creates the association between the vault and its supported token type
- The registration enables the NCN program to track vault delegations for voting power calculation
This registration process establishes the complete set of vaults that can contribute to the voting system, creating a closed ecosystem of verified participants.
5.6 NCN Architecture and Security Considerations
5.5 Architecture Considerations
The NCN program configuration establishes a multi-layered security model:
- Authentication Layer: Only the NCN admin can initialize configuration and register tokens
- Relationship Layer: Only vaults and operators with established, active handshakes can participate
- Time Security Layer: Enforced waiting periods prevent quick creation and use of malicious actors
- Registry Layer: All participants must be registered and tracked in on-chain registries
This layered approach ensures the integrity of the voting system by validating the identity and relationships of all participants before they can influence the consensus process.
The configuration phase completes the preparation of the system’s infrastructure, setting the stage for the actual voting mechanics to begin in subsequent phases.
6. Epoch Snapshot and Voting Preparation
The Epoch Snapshot and Voting Preparation phase is where the system captures the current state of all participants and prepares the infrastructure for voting. This is an essential component of the architecture as it ensures voting is based on a consistent, verifiable snapshot of the network state at a specific moment in time.
The upcoming section is a keeper task (with the exception of the voting). This means that it is permissionless and can be done by anyone.
6.1 Epoch State Initialization
To begin a new consensus cycle (epoch), we first initialize an EpochState
account for our NCN, which will track the progress of this epoch.
Copy and paste the following code at the bottom of your test function:
// Initialize the epoch state for the current epoch
fixture.add_epoch_state_for_test_ncn(&test_ncn).await?;
This step initializes the Epoch State for the current consensus cycle:
- It creates an
EpochState
account tied to the specific NCN and epoch. - This account tracks the progress through each stage of the consensus cycle.
- It maintains flags for each phase (weight setting, snapshot taking, voting, closing).
- The epoch state provides protection against out-of-sequence operations.
- It stores metadata like the current epoch, slot information, and participant counts.
Once initialized, the EpochState
account becomes the authoritative record of where the system is in the voting process, preventing operations from happening out of order or in duplicate.
You can take a look at the epoch state struct here.
6.2 Weight Table Initialization and Population
For the current epoch, we initialize a WeightTable
and populate it by copying the token weights from the VaultRegistry
, effectively freezing these weights for the duration of this consensus cycle.
Copy and paste the following code at the bottom of your test function:
// Initialize the weight table to track voting weights
let clock = fixture.clock().await;
let epoch = clock.epoch;
ncn_program_client
.do_full_initialize_weight_table(test_ncn.ncn_root.ncn_pubkey, epoch)
.await?;
// Take a snapshot of weights for each token mint
ncn_program_client
.do_set_epoch_weights(test_ncn.ncn_root.ncn_pubkey, epoch)
.await?;
The weight table mechanism handles the token weights for the current epoch in two stages:
-
Weight Table Initialization:
- Creates a
WeightTable
account for the specific epoch usingdo_full_initialize_weight_table
. This may involve multiple calls internally to allocate sufficient space. - Allocates space based on the number of supported tokens registered in the
VaultRegistry
. - Links the table to the NCN and current epoch.
- Initializes the table structure with empty entries.
- Creates a
-
Weight Setting:
- Populates the
WeightTable
by callingdo_set_epoch_weights
- Copies the current weights from the
VaultRegistry
to the epoch-specificWeightTable
. - “Freezes” these weights for the duration of the consensus cycle.
- Updates the
EpochState
to mark weight setting as complete. - Creates an immutable record of token weights that will be used for voting.
- Populates the
This two-step process is critical for the integrity of the system as it:
- Creates a permanent record of weights at the time voting begins.
- Prevents weight changes during a consensus cycle from affecting ongoing votes.
- Allows transparent verification of the weights used for a particular vote.
- Enables historical auditing of how weights changed over time.
6.3 Epoch Snapshot Creation
We then create an EpochSnapshot
account to record the overall state for this epoch, such as total operator and vault counts, and to accumulate total stake weight.
Copy and paste the following code at the bottom of your test function:
// Take the epoch snapshot
fixture.add_epoch_snapshot_to_test_ncn(&test_ncn).await?;
The epoch snapshot captures the aggregate state of the entire system:
- Creates an
EpochSnapshot
account for the NCN and epoch. - Records the total number of operators and vaults expected to participate.
- Captures the total potential stake weight across all participants (initialized to zero).
- Stores important metadata like the snapshot creation slot.
- Serves as the reference point for total voting power calculations, acting as the denominator for consensus thresholds.
6.4 Operator Snapshots
Next, individual OperatorSnapshot
accounts are created for each participating operator, capturing their state and expected delegations for the epoch.
Copy and paste the following code at the bottom of your test function:
// 2.b. Initialize the operators using the Jito Restaking program, and initiate the
// handshake relationship between the NCN <> operators
{
for _ in 0..OPERATOR_COUNT {
// Set operator fee to 100 basis points (1%)
let operator_fees_bps: Option<u16> = Some(100);
// Initialize a new operator account with the specified fee
let operator_root = restaking_client
.do_initialize_operator(operator_fees_bps)
.await?;
// Establish bidirectional handshake between NCN and operator:
// 1. Initialize the NCN's state tracking (the NCN operator ticket) for this operator
restaking_client
.do_initialize_ncn_operator_state(
&test_ncn.ncn_root,
&operator_root.operator_pubkey,
)
.await?;
// 2. Advance slot to satisfy timing requirements
fixture.warp_slot_incremental(1).await.unwrap();
// 3. NCN warms up to operator - creates NCN's half of the handshake
restaking_client
.do_ncn_warmup_operator(&test_ncn.ncn_root, &operator_root.operator_pubkey)
.await?;
// 4. Operator warms up to NCN - completes operator's half of the handshake
restaking_client
.do_operator_warmup_ncn(&operator_root, &test_ncn.ncn_root.ncn_pubkey)
.await?;
// Add the initialized operator to our test NCN's operator list
test_ncn.operators.push(operator_root);
}
}
This step creates an individual snapshot for each operator in the system:
- For each operator, it creates an
OperatorSnapshot
account linked to the operator, NCN, and epoch. - Records the operator’s total delegated stake weight at this moment (initialized to zero).
- Captures the expected number of vault delegations for the operator.
- Verifies the operator has active handshakes with the NCN.
- Validates the operator’s eligibility to participate in voting.
These snapshots establish each operator’s baseline for the current epoch. The actual voting power will be populated in the next step based on individual delegations. This ensures that later delegation changes cannot alter voting weight once the snapshot phase is complete.
6.5 Vault-Operator Delegation Snapshots
With operator snapshots ready, we now record the weighted stake from each specific vault-to-operator delegation into the relevant OperatorSnapshot
and update the total stake in the EpochSnapshot
.
Copy and paste the following code at the bottom of your test function:
// Record all vault-to-operator delegations
fixture
.add_vault_operator_delegation_snapshots_to_test_ncn(&test_ncn)
.await?;
This crucial step iterates through each active vault-to-operator delegation and records its contribution to the operator’s voting power:
- For each valid delegation found in the Jito Vault program:
- Retrieves the corresponding token weight from the epoch’s
WeightTable
. - Calculates the weighted stake for that delegation (delegation amount * token weight).
- Updates the relevant
OperatorSnapshot
by adding the calculated stake weight. - Stores detailed information about the weighted delegation within the
OperatorSnapshot
’svault_operator_stake_weight
array. - Increments the total stake weight in the global
EpochSnapshot
. - Creates a
VaultOperatorDelegationSnapshot
account for detailed auditing.
- Retrieves the corresponding token weight from the epoch’s
These granular snapshots serve multiple purposes:
- They populate the
OperatorSnapshot
accounts with the actual stake weights used for voting. - They update the
EpochSnapshot
with the total voting power present in the system for this epoch. - They provide detailed audit trails of exactly where each operator’s voting power originates.
- They enable verification of correct weight calculation for each delegation.
- They prevent retroactive manipulation of the voting power distribution.
6.6 Ballot Box Initialization
To prepare for voting, we initialize a BallotBox
account for the current epoch, which will collect and tally all operator votes.
Copy and paste the following code at the bottom of your test function:
// Initialize the ballot box for collecting votes
fixture.add_ballot_box_to_test_ncn(&test_ncn).await?;
The final preparation step creates the ballot box:
- Initializes a
BallotBox
account linked to the NCN and epoch usingdo_full_initialize_ballot_box
. Similar to the weight table, this may require multiple allocation calls internally. - Creates arrays to track operator votes (
OperatorVote
) and ballot tallies (BallotTally
). - Sets up the data structures for recording and counting votes.
- Prepares the consensus tracking mechanism.
- Links the ballot box to the
EpochState
for progress tracking.
The BallotBox
becomes the central repository where all votes are recorded and tallied during the voting process. It is designed to efficiently track:
- Which operators have voted and what they voted for.
- The cumulative stake weight behind each voting option (ballot).
- The current winning ballot (if any).
- Whether consensus has been reached.
6.7 Snapshot Architecture and Security Considerations
The snapshot system implements several key architectural principles:
- Point-in-Time Consistency: All snapshots capture the system state relative to the start of the epoch, creating a consistent view based on frozen weights and delegations present at that time.
- Immutability: Once taken and populated, snapshots cannot be modified, ensuring the integrity of the voting weights used.
- Layered Verification: The system enables verification at multiple levels:
- Aggregate level (
EpochSnapshot
) - Participant level (
OperatorSnapshot
) - Relationship level (individual weighted delegations within
OperatorSnapshot
, optionallyVaultOperatorDelegationSnapshot
)
- Aggregate level (
- Defense Against Time-Based Attacks: By freezing the state (weights and relevant delegations) before voting begins, the system prevents:
- Late stake additions influencing outcomes within the current epoch.
- Strategic withdrawals affecting voting power after the snapshot.
- Any form of “stake voting power front-running” within the epoch.
- Separation of State and Process:
- The state (snapshots, weights) is captured separately from the process (voting).
- This clear separation simplifies reasoning about the system.
- It enables more effective testing and verification.
The comprehensive snapshot approach ensures that voting occurs on a well-defined, verifiable view of the network’s state, establishing a solid foundation for the actual voting process to follow.
7. Voting Process
The Voting Process is the core functionality of the NCN system, where operators express their preferences on the network state (represented by the “weather status” in this simulation). This process leverages the infrastructure and snapshots created in previous steps to ensure secure, verifiable, and stake-weighted consensus.
7.1 Setting the Expected Outcome
In our simulation, we’ll predefine an expected winning outcome for verification purposes.
Copy and paste the following code at the bottom of your test function:
// Define the expected winning weather status
let winning_weather_status = WeatherStatus::Sunny as u8;
For testing purposes, the system defines an expected outcome (WeatherStatus::Sunny
). In a production environment, the winning outcome would be determined organically through actual operator votes based on real-world data or criteria. The weather status enum (Sunny
, Cloudy
, Rainy
) serves as a simplified proxy for any on-chain decision that requires consensus.
7.2 Casting Votes from Different Operators
Operators now cast their votes. We’ll simulate a few operators voting, some for the expected outcome and some against, to test the tallying logic.
Copy and paste the following code at the bottom of your test function:
// Cast votes from operators
{
let epoch = fixture.clock().await.epoch;
let first_operator = &test_ncn.operators[0];
let second_operator = &test_ncn.operators[1];
let third_operator = &test_ncn.operators[2];
// First operator votes for Cloudy
ncn_program_client
.do_cast_vote(
ncn_pubkey,
first_operator.operator_pubkey,
&first_operator.operator_admin,
WeatherStatus::Cloudy as u8,
epoch,
)
.await?;
// Second and third operators vote for Sunny (expected winner)
ncn_program_client
.do_cast_vote(
ncn_pubkey,
second_operator.operator_pubkey,
&second_operator.operator_admin,
winning_weather_status,
epoch,
)
.await?;
ncn_program_client
.do_cast_vote(
ncn_pubkey,
third_operator.operator_pubkey,
&third_operator.operator_admin,
winning_weather_status,
epoch,
)
.await?;
}
This section demonstrates the system’s ability to handle diverse voting preferences using the do_cast_vote
helper, which calls the cast_vote
instruction:
- The first operator votes for “Cloudy” (representing a minority view).
- The second and third operators vote for “Sunny” (the presumed majority view).
- Each
do_cast_vote
call invokes the NCN program with the operator’s choice and admin signature.
Under the hood, each vote triggers several key operations within the cast_vote
instruction:
- Verification:
- Verifies the operator admin’s signature.
- Checks that the operator hasn’t already voted in this epoch using the
BallotBox
. - Retrieves the operator’s
OperatorSnapshot
to confirm eligibility and get its total stake weight. - Ensures the
EpochState
indicates voting is currently allowed.
- Recording:
- Records the vote details (operator, slot, stake weight, ballot choice) in the
operator_votes
array within theBallotBox
. - Marks the operator as having voted.
- Records the vote details (operator, slot, stake weight, ballot choice) in the
- Tallying:
- Finds or creates a
BallotTally
for the chosen weather status in theballot_tallies
array. - Adds the operator’s full stake weight (from the snapshot) to this tally.
- Increments the raw vote count for this tally.
- Finds or creates a
- Consensus Check:
- Compares the updated tally’s stake weight against the total stake weight recorded in the
EpochSnapshot
. - If the tally now exceeds the consensus threshold (e.g., 66%), it marks consensus as reached in the
BallotBox
and records the current slot.
- Compares the updated tally’s stake weight against the total stake weight recorded in the
7.3 Establishing Consensus Through Majority Voting
To ensure consensus is reached for our test, the remaining eligible operators will now vote for the predefined winning weather status.
Copy and paste the following code at the bottom of your test function:
// All remaining operators vote for Sunny to form a majority
for operator_root in test_ncn.operators.iter().take(OPERATOR_COUNT).skip(3) {
ncn_program_client
.do_cast_vote(
ncn_pubkey,
operator_root.operator_pubkey,
&operator_root.operator_admin,
winning_weather_status,
epoch,
)
.await?;
}
The consensus mechanism works as follows:
- The system maintains a running
BallotTally
for each unique option voted on. - After each vote, it recalculates the total stake weight supporting the voted option.
- It compares this stake weight to the total stake weight available in the
EpochSnapshot
. - If an option’s stake weight reaches the consensus threshold (e.g., >= 66%), the system:
- Marks that
Ballot
as thewinning_ballot
in theBallotBox
. - Records the current
slot
inslot_consensus_reached
. - Updates the
EpochState
. - Creates a persistent
ConsensusResult
account (discussed in Verification).
- Marks that
- Consensus requires a supermajority to ensure decisions have strong, verifiable support across the network’s weighted stake.
7.4 Vote Processing Architecture
When an operator casts a vote via the cast_vote
instruction, the system performs several critical operations:
- Authentication: Verifies the transaction is signed by the correct
operator_admin
keypair associated with theoperator
account. - Authorization & Preconditions: Confirms that:
- The operator exists, is registered with the NCN, and has an active
OperatorSnapshot
for the currentepoch
. - The operator has not already voted in this epoch (checked via
BallotBox
). - The operator has non-zero stake weight in their
OperatorSnapshot
. - The
EpochState
confirms that the snapshotting phase is complete and voting is open.
- The operator exists, is registered with the NCN, and has an active
- Vote Recording:
- Locates an empty slot or confirms the operator hasn’t voted in the
operator_votes
array within theBallotBox
. - Stores the
operator
pubkey, currentslot
, the operator’s totalstake_weights
(fromOperatorSnapshot
), and the index corresponding to the chosen ballot within theballot_tallies
array. - Increments the
operators_voted
counter in theBallotBox
.
- Locates an empty slot or confirms the operator hasn’t voted in the
- Ballot Processing & Tallying:
- Searches the
ballot_tallies
array for an existing entry matching theweather_status
. - If found: Adds the operator’s
stake_weights
to thestake_weights
field of the existingBallotTally
and increments the rawtally
counter. - If not found: Initializes a new
BallotTally
entry with theweather_status
, the operator’sstake_weights
, and atally
of 1. Incrementsunique_ballots
.
- Searches the
- Consensus Calculation & Result Creation:
- Retrieves the total
stake_weights
from theEpochSnapshot
. - Compares the winning ballot’s accumulated
stake_weights
against the total. - If the threshold is met and consensus hasn’t already been marked:
- Sets the
winning_ballot
field in theBallotBox
. - Records the current
slot
inslot_consensus_reached
. - Updates the
EpochState
. - Invokes an instruction (likely via CPI or separate transaction) to create the
ConsensusResult
account, storing the winning status, epoch, weights, and slot.
- Sets the
- Retrieves the total
- Cross-Validation: Implicitly ensures the vote aligns with the correct
ncn
andepoch
through the PDAs used for the involved accounts (BallotBox
,OperatorSnapshot
,EpochState
).
This multi-layered architecture ensures votes are processed securely, tallied correctly using the snapshotted weights, and that consensus is determined accurately based on stake-weighted participation.
7.5 Security Considerations in the Voting Process
The voting process incorporates several key security features:
- Sybil Attack Prevention:
- Voting power is derived directly from snapshotted stake weight, not operator count.
- Operators with zero snapshotted stake weight cannot vote, preventing attacks based on creating numerous fake operators.
- Replay Protection:
- The
BallotBox
tracks which operators have voted (operator_votes
array). - Attempts by an operator to vote more than once within the same epoch are rejected.
- The
- Time-Bound Voting:
- Votes are only accepted if the
EpochState
indicates the voting phase is active for the specifiedepoch
. - While votes might be accepted slightly after consensus is reached (within
valid_slots_after_consensus
), they won’t change the already determined outcome.
- Votes are only accepted if the
- Authority: Requires
operator_admin
signature. - Tamper-Proof Tallying: Uses immutable snapshotted data created before voting began.
- Consistent Threshold: Calculated based on the total stake weight recorded in the
EpochSnapshot
, providing a fixed target for the epoch.
These security measures ensure the voting process remains resilient against various attack vectors and manipulation attempts, maintaining the integrity of the consensus mechanism.
8. Rewards Distribution
After consensus is reached, the NCN system can distribute rewards to participants based on their contributions to the consensus process. The rewards system operates through a multi-layered distribution mechanism that allocates rewards to different stakeholders: the Protocol, the NCN itself, operators, and vaults.
The reward distribution process consists of three main phases:
- Router Initialization: Setting up the infrastructure for reward routing
- NCN Reward Routing and Distributing: Routing and distributing rewards according to the fee structure to the protocol and the NCN, and to the Operator_Vault couples
- Operator Vault Reward Routing and Distributing: Routing and distributing rewards to operators and their delegated vaults
8.1 Reward Router Initialization
Before rewards can be distributed, the system must initialize reward routers that manage the flow of rewards to different participants.
Copy and paste the following code at the bottom of your test function:
// Setup reward routers for NCN and operators
{
let ncn = test_ncn.ncn_root.ncn_pubkey;
let clock = fixture.clock().await;
let epoch = clock.epoch;
ncn_program_client
.do_full_initialize_ncn_reward_router(ncn, epoch)
.await?;
for operator_root in test_ncn.operators.iter() {
let operator = operator_root.operator_pubkey;
ncn_program_client
.do_initialize_operator_vault_reward_router(ncn, operator, epoch)
.await?;
}
}
This step creates the infrastructure for reward distribution:
- NCN Reward Router: A primary router that receives all rewards and distributes them according to the configured fee structure. It manages the overall reward pool and calculates allocations for Protocol, NCN, and operator rewards.
- Operator Vault Reward Routers: Individual routers for each operator that manage the distribution of rewards to operators and their associated vaults. These handle the final distribution to operators and their delegated vaults.
The reward routers implement a hierarchical distribution system:
- All rewards initially flow into the NCN Reward Router
- The NCN Reward Router distributes rewards based on fee configurations
- Operator-specific rewards flow through Operator Vault Reward Routers
- Finally, rewards reach the ultimate recipients (operators and vault holders)
8.2 NCN Reward Routing and Distribution
The first phase of reward distribution involves routing rewards into the NCN system and distributing them according to the configured fee structure.
Copy and paste the following code at the bottom of your test function:
// Route rewards into the NCN reward system
{
let ncn = test_ncn.ncn_root.ncn_pubkey;
let epoch = fixture.clock().await.epoch;
const REWARD_AMOUNT: u64 = 1_000_000;
// Advance the clock to ensure we are in a valid time window for reward distribution.
let valid_slots_after_consensus = {
let config = ncn_program_client.get_ncn_config(ncn).await?;
config.valid_slots_after_consensus()
};
fixture
.warp_slot_incremental(valid_slots_after_consensus + 1)
.await?;
// Send rewards to the NCN reward receiver
let ncn_reward_receiver =
NCNRewardReceiver::find_program_address(&ncn_program::id(), &ncn, epoch).0;
fn lamports_to_sol(lamports: u64) -> f64 {
lamports as f64 / 1_000_000_000.0
}
let sol_rewards = lamports_to_sol(REWARD_AMOUNT);
ncn_program_client
.airdrop(&ncn_reward_receiver, sol_rewards)
.await?;
// Route rewards through the NCN reward system
ncn_program_client.do_route_ncn_rewards(ncn, epoch).await?;
// Should be able to route twice (idempotent operation)
ncn_program_client.do_route_ncn_rewards(ncn, epoch).await?;
let ncn_reward_router = ncn_program_client.get_ncn_reward_router(ncn, epoch).await?;
// Distribute Protocol Rewards (4% of total)
{
let rewards = ncn_reward_router.protocol_rewards();
if rewards > 0 {
let config = ncn_program_client.get_ncn_config(ncn).await?;
let protocol_fee_wallet = config.fee_config.protocol_fee_wallet();
let balance_before = {
let account = fixture.get_account(protocol_fee_wallet).await?;
account.unwrap().lamports
};
println!("Distributing {} of Protocol Rewards", rewards);
ncn_program_client
.do_distribute_protocol_rewards(ncn, epoch)
.await?;
let balance_after = {
let account = fixture.get_account(protocol_fee_wallet).await?;
account.unwrap().lamports
};
assert_eq!(
balance_after,
balance_before + rewards,
"Protocol fee wallet balance should increase by the rewards amount"
);
}
}
// Distribute NCN Rewards (4% of total)
{
let rewards = ncn_reward_router.ncn_rewards();
if rewards > 0 {
let config = ncn_program_client.get_ncn_config(ncn).await?;
let ncn_fee_wallet = config.fee_config.ncn_fee_wallet();
let balance_before = {
let account = fixture.get_account(ncn_fee_wallet).await?;
account.unwrap().lamports
};
println!("Distributing {} of NCN Rewards", rewards);
ncn_program_client
.do_distribute_ncn_rewards(ncn, epoch)
.await?;
let balance_after = {
let account = fixture.get_account(ncn_fee_wallet).await?;
account.unwrap().lamports
};
assert_eq!(
balance_after,
balance_before + rewards,
"NCN fee wallet balance should increase by the rewards amount"
);
}
}
// Distribute Operator Vault Rewards (92% of total)
{
for operator_root in test_ncn.operators.iter() {
let operator = operator_root.operator_pubkey;
let operator_route = ncn_reward_router.operator_vault_reward_route(&operator);
let rewards = operator_route.rewards().unwrap_or(0);
if rewards == 0 {
continue;
}
println!("Distribute NCN Reward {}", rewards);
ncn_program_client
.do_distribute_operator_vault_reward_route(operator, ncn, epoch)
.await?;
}
}
}
The NCN reward routing process follows these steps:
- Timing Validation: The system waits for the configured
valid_slots_after_consensus
period to ensure proper timing for reward distribution. - Reward Reception: Rewards are deposited into the NCN Reward Receiver account, which serves as the entry point for all rewards.
- Fee Calculation: The system automatically calculates different fee categories based on the NCN configuration:
- Protocol Fees: 4% allocated to the Protocol for maintaining the underlying restaking infrastructure
- NCN Fees: 4% retained by the NCN for operational costs
- Operator Vault Rewards: 92% allocated to operators and their delegated vaults
- Distribution Execution: Each category of rewards is distributed to its respective recipients:
- Protocol Rewards: Transferred directly to the configured Protocol fee wallet
- NCN Rewards: Transferred to the NCN’s fee wallet
- Operator Vault Rewards: Routed to individual Operator Vault Reward Routers for further distribution
The distribution is weighted based on the operators’ voting participation and stake weights from the consensus process, ensuring that rewards flow proportionally to participants who contributed to achieving consensus.
8.3 Operator Vault Reward Routing
The second phase distributes rewards that were allocated to operators and vaults, managing the final distribution to individual participants.
Copy and paste the following code at the bottom of your test function:
// Route rewards to operators and their delegated vaults
{
let ncn = test_ncn.ncn_root.ncn_pubkey;
let epoch = fixture.clock().await.epoch;
for operator_root in test_ncn.operators.iter() {
let operator = operator_root.operator_pubkey;
// Route rewards to operator and vaults
ncn_program_client
.do_route_operator_vault_rewards(ncn, operator, epoch)
.await?;
// Should be able to route twice (idempotent operation)
ncn_program_client
.do_route_operator_vault_rewards(ncn, operator, epoch)
.await?;
let operator_vault_reward_router = ncn_program_client
.get_operator_vault_reward_router(operator, ncn, epoch)
.await?;
// Distribute operator's fee portion
let operator_rewards = operator_vault_reward_router.operator_rewards();
if operator_rewards > 0 {
ncn_program_client
.do_distribute_operator_rewards(operator, ncn, epoch)
.await?;
}
// Distribute rewards to vaults that delegated to this operator
for vault_root in test_ncn.vaults.iter() {
let vault = vault_root.vault_pubkey;
let vault_reward_route = operator_vault_reward_router.vault_reward_route(&vault);
if let Ok(vault_reward_route) = vault_reward_route {
let vault_rewards = vault_reward_route.rewards();
if vault_rewards > 0 {
ncn_program_client
.do_distribute_vault_rewards(vault, operator, ncn, epoch)
.await?;
}
}
}
}
}
The operator vault reward routing process manages distribution at the most granular level:
- Operator Fee Calculation: Each operator’s configured fee (basis points) is calculated and retained by the operator. This fee is deducted from the total rewards allocated to that operator before vault distribution.
- Vault Reward Distribution: The remaining rewards are distributed to vaults that delegated stake to the operator, proportional to their delegation amounts and token weights.
- Proportional Allocation: Rewards are allocated based on:
- Delegation Weight: Larger delegations receive proportionally more rewards
- Token Weight: Different token types contribute different weighted values based on the weight table
- Participation: Only delegations that contributed to the voting process receive rewards
- Idempotent Operations: The routing operations are designed to be idempotent, meaning they can be called multiple times without adverse effects, ensuring reliability in distributed systems.
This ensures that the economic incentives align with the security and participation goals of the NCN system.
8.4 Reward Architecture and Considerations
The rewards system implements several key architectural principles:
- Multi-Tier Distribution:
- Infrastructure Level: Protocol receives 4% fees for maintaining the underlying restaking infrastructure
- Network Level: NCN receives 4% fees for operating the consensus network
- Operator Level: Operators receive their configured fee percentage for participation and validation services
- Delegator Level: Vault holders receive proportional rewards for providing stake
- Proportional Incentives:
- Rewards are distributed proportionally to stake weight contributions from the epoch snapshot
- Higher token weights result in higher reward allocations
- Active participation in voting is required to receive rewards
- Only operators with valid stake delegations can receive rewards
- Configurable Fee Structure:
- Protocol and NCN fees are set at 4% each in the current implementation
- Operator fees are individually configurable (e.g., 100 basis points = 1%)
- The system supports flexible reward allocation policies through configuration
- Economic Security:
- Reward distribution aligns economic incentives with network security
- Participants are rewarded for honest behavior and penalized for non-participation
- The system creates sustainable incentives for long-term network health
- Rewards are only distributed after consensus is reached
- Transparency and Auditability:
- All reward distributions are recorded on-chain with detailed routing accounts
- The calculation methodology is transparent and verifiable through the reward router accounts
- Historical reward data enables analysis of network economics
- Balance checks ensure accurate reward distribution
- Reliability and Safety:
- Timing constraints ensure rewards are only distributed after consensus finalization
- Idempotent operations prevent double-spending or incorrect distributions
- Balance verification ensures rewards are correctly transferred to recipients
This comprehensive reward system ensures that all participants in the NCN ecosystem are appropriately compensated for their contributions while maintaining the security and integrity of the consensus mechanism.
9. Verification
The Verification phase validates that the voting process completed successfully and that the expected consensus was achieved. This critical step confirms the integrity of the entire system by examining the on-chain data structures (BallotBox
and ConsensusResult
) and verifying they contain the expected results.
9.1 Ballot Box Verification
After voting concludes, we first verify the BallotBox
to ensure it correctly reflects that consensus was reached and identifies the expected winning ballot.
Copy and paste the following code at the bottom of your test function:
// Verify the results recorded in the BallotBox
{
let epoch = fixture.clock().await.epoch;
let ballot_box = ncn_program_client.get_ballot_box(ncn_pubkey, epoch).await?;
assert!(ballot_box.has_winning_ballot());
assert!(ballot_box.is_consensus_reached());
assert_eq!(ballot_box.get_winning_ballot().unwrap().weather_status(), winning_weather_status);
}
The first verification step examines the BallotBox
account for the completed epoch:
- Winning Ballot Check:
has_winning_ballot()
confirms that thewinning_ballot
field within theBallotBox
structure is marked as valid.
-
Consensus Status Check:
- Winning Ballot Check:
has_winning_ballot()
confirms that thewinning_ballot
field within theBallotBox
structure is marked as valid.
- Consensus Status Check:
is_consensus_reached()
checks if theslot_consensus_reached
field is greater than zero, indicating the consensus condition was met during the voting process.
- Outcome Verification:
- The test retrieves the
winning_ballot
struct and asserts that itsweather_status
field matches thewinning_weather_status
defined earlier (WeatherStatus::Sunny
). This confirms the correct outcome was identified based on the stake-weighted tally.
- The test retrieves the
Verifying the BallotBox
ensures the core voting and tallying mechanism functioned correctly during the active epoch.
9.2 Consensus Result Account Verification
Next, we verify the permanently stored ConsensusResult
account to confirm it accurately records the winning outcome, epoch details, and vote weights, consistent with the BallotBox
.
Copy and paste the following code at the bottom of your test function:
// Fetch and verify the consensus_result account
{
let epoch = fixture.clock().await.epoch;
let consensus_result = ncn_program_client
.get_consensus_result(ncn_pubkey, epoch)
.await?;
assert!(consensus_result.is_consensus_reached());
assert_eq!(consensus_result.epoch(), epoch);
assert_eq!(consensus_result.weather_status(), winning_weather_status);
let ballot_box = ncn_program_client.get_ballot_box(ncn_pubkey, epoch).await?;
let winning_ballot_tally = ballot_box.get_winning_ballot_tally().unwrap();
assert_eq!(consensus_result.vote_weight(), winning_ballot_tally.stake_weights().stake_weight() as u64);
println!(
"✅ Consensus Result Verified - Weather Status: {}, Vote Weight: {}, Total Weight: {}, Recorder: {}",
consensus_result.weather_status(),
consensus_result.vote_weight(),
consensus_result.total_vote_weight(),
consensus_result.consensus_recorder()
);
}
The second verification step examines the ConsensusResult
account, which serves as the permanent, immutable record of the voting outcome:
- Consensus Result Existence & Fetching:
- The test successfully fetches the
ConsensusResult
account using its PDA derived from the NCN pubkey and epoch. Its existence implies consensus was reached and the account was created.
- The test successfully fetches the
-
Consensus Status Validation:
- Consensus Result Existence & Fetching:
- The test successfully fetches the
ConsensusResult
account using its PDA derived from the NCN pubkey and epoch. Its existence implies consensus was reached and the account was created.
- The test successfully fetches the
- Consensus Status Validation:
is_consensus_reached()
checks an internal flag derived from stored values (likeconsensus_slot
> 0), confirming the outcome is officially recognized.
- Metadata Verification:
- Asserts that the
epoch
field matches the current epoch. - Asserts that the
weather_status
matches the expectedwinning_weather_status
.
- Asserts that the
- Cross-Account Consistency Check:
- Fetches the
BallotBox
again. - Retrieves the
BallotTally
corresponding to the winning ballot from theBallotBox
. - Asserts that the
vote_weight
stored in theConsensusResult
exactly matches thestake_weight
recorded in the winningBallotTally
within theBallotBox
. This ensures consistency between the temporary voting record and the permanent result.
- Fetches the
- Detailed Reporting:
- Prints key details from the verified
ConsensusResult
account for confirmation.
- Prints key details from the verified
Verifying the ConsensusResult
confirms that the outcome was durably stored with the correct details and consistent with the voting process itself.
9.3 Architecture of Verification and Result Persistence
The verification phase highlights several important architectural features:
- Dual Records:
- The system temporarily uses the
BallotBox
during the epoch for active voting and tallying. - Upon reaching consensus, it creates a separate, permanent
ConsensusResult
account. - This redundancy allows for cleanup while preserving the essential outcome.
- The system temporarily uses the
- Separation of Process and Outcome:
- The
BallotBox
(process) can eventually be closed to reclaim rent. - The
ConsensusResult
(outcome) persists indefinitely as the historical record.
- The
- Automated Result Creation:
- The
ConsensusResult
account is typically created automatically within thecast_vote
instruction when the consensus threshold is first met. This ensures timely recording without requiring a separate administrative action.
- The
- Result Immutability:
- The
ConsensusResult
account, once created, is designed to be immutable. It stores the outcome based on the state when consensus was reached.
- The
- Time and Slot Tracking:
- Both
BallotBox
andConsensusResult
store key timing information (slot_consensus_reached
,epoch
). This metadata is crucial for auditing and understanding the system’s behavior over time.
- Both
9.4 Verification Techniques and Best Practices
The verification approach demonstrates several best practices:
- Multi-Level Verification: Testing both the ephemeral process account (
BallotBox
) and the persistent outcome account (ConsensusResult
) provides comprehensive validation. - State Assertions: Using dedicated helper functions on the deserialized accounts (
has_winning_ballot()
,is_consensus_reached()
) makes tests more readable and robust against internal representation changes. - Equality Assertions: Using strict equality (
assert_eq!
) for key outcome data (winning status, epoch, weights) ensures exactness. - Cross-Structure Validation: Comparing critical values (like
vote_weight
) between theBallotBox
andConsensusResult
confirms data consistency across different parts of the system. - Complete Outcome Validation: Checking not just the winning choice but also associated metadata (epoch, weights, consensus flags) catches more subtle errors.
- Clear Reporting: Outputting verified data (
println!
) provides immediate feedback during test runs.
This rigorous verification ensures the NCN system reliably achieves and records stake-weighted consensus according to its design.
10. Cleanup
After the core functionality has been tested and verified for a given epoch, the temporary accounts associated with that epoch can be closed to reclaim the SOL locked for rent. The persistent ConsensusResult
account remains.
Copy and paste the following code at the bottom of your test function:
// Close epoch accounts but keep consensus result
let epoch_before_closing_account = fixture.clock().await.epoch;
fixture.close_epoch_accounts_for_test_ncn(&test_ncn).await?;
// Verify that consensus_result account is not closed
{
let consensus_result = ncn_program_client
.get_consensus_result(ncn_pubkey, epoch_before_closing_account)
.await?;
assert!(consensus_result.is_consensus_reached());
assert_eq!(consensus_result.epoch(), epoch_before_closing_account);
}
This cleanup process involves:
- Identifying Epoch: Recording the current epoch (
epoch_before_closing_account
) just before initiating closure. - Closing Accounts: Calling
fixture.close_epoch_accounts_for_test_ncn
, which likely iterates through epoch-specific accounts and invokes aclose_epoch_account
instruction for each. - Verifying Persistence: After the cleanup function returns, the test attempts to fetch the
ConsensusResult
account for the sameepoch_before_closing_account
. - Confirming Data: It asserts that the fetched
ConsensusResult
still exists and retains its key data (is_consensus_reached
,epoch
), confirming it was not closed during the cleanup process.
This demonstrates a crucial design feature:
- Resource Management: Temporary accounts are removed, preventing indefinite accumulation of rent-paying accounts.
- Outcome Preservation: The final, critical outcome (
ConsensusResult
) is preserved as a permanent on-chain record, suitable for historical lookups or use by other programs.
This efficient cleanup mechanism allows the NCN system to operate continuously over many epochs without unbounded growth in account storage requirements.
Now you can save the file and run the test to see the result.
NCN Keeper
Each NCN relies on off-chain agents called keepers. Keepers are essentially permissionless automation agents that execute all necessary on-chain instructions to advance (“crank”) the NCN through its epoch phases. Anyone can run a keeper. There are no special authorities required to keep the NCN operational. By monitoring network state and calling the NCN program’s instructions at certain times, keepers make sure the NCN progresses correctly and remains in sync with Solana’s epoch.
This guide provides an overview of how to use the ncn-program-cli
, a command-line interface for interacting with an NCN program using the NCN template. Below, we cover installation, configuration, and step-by-step usage of the CLI, from initial setup through running the NCN keeper to automate state management.
Installation and Setup
Before using the Template NCN Program CLI, ensure you have it installed and configured properly, along with the related Jito (Re)Staking tools:
-
Build and install the NCN Program CLI: If you have the NCN program template repo, compile and install the CLI binary. For example, using Cargo:
# Clone the template repo git clone [email protected]:jito-foundation/ncn-template.git cd ncn-template # Build the CLI from the repository (assuming you're in the repo directory) cargo build --release # Install the CLI binary cargo install --path ./cli --bin ncn-program-cli --locked
After installation, verify it works by running:
ncn-program-cli --help
This should display the general help and list available subcommands.
-
Install Jito (Re)Staking CLI (if not already): The NCN program operates alongside Jito’s restaking program. You may need the Jito (Re)Staking CLI (
jito-restaking-cli
) to manage restaking registry tasks (like registering NCNs, operators, and vaults). Install it using Cargo:cargo install jito-restaking-cli
Confirm it is installed:
jito-restaking-cli --help
-
Configure Environment Variables: The
ncn-program-cli
accepts configuration through command-line options or environment variables. Optionally, to avoid passing flags every time, you can use a.env
file for convenience:# NCN Operator & Program CLI Environment Configuration # Copy this file to `.env` and update the values below # --------------- REQUIRED -------------------- # Solana cluster (mainnet, devnet, testnet, or localnet) CLUSTER=devnet # RPC endpoint for your Solana cluster (must support getBlock and transaction history) RPC_URL=https://api.devnet.solana.com # Commitment level for RPC operations (e.g. confirmed or finalized) COMMITMENT=confirmed # On-chain NCN instance address (created by the NCN admin) NCN=<Your_NCN_account_address> # Path to your Solana keypair file (must have admin/operator authority) KEYPAIR_PATH=~/.config/solana/id.json # Operator public key (the account responsible for voting) OPERATOR=BSia35bXHZx69XzCQeMUnWqZJsUwJURVvuUg8Jup2BcP # OpenWeather API key (used by the example weather oracle operator) OPENWEATHER_API_KEY=your_api_key_here # --------------- PROGRAM IDS -------------------- # Use these only if you are deploying custom programs # Otherwise, leave them blank to use defaults # NCN Program ID (default: 7rNw1g2ZUCdTrCyVGZwCJLnbp3ssTRK5mdkH8gm9AKE8) NCN_PROGRAM_ID= # Jito Restaking program (default value) RESTAKING_PROGRAM_ID=RestkWeAVL8fRGgzhfeoqFhsqKRchg6aa1XrcH96z4Q # Jito Vault program (default value) VAULT_PROGRAM_ID=Vau1t6sLNxnzB7ZDsef8TLbPLfyZMYXH8WTNqUdm9g8 # --------------- LOGGING -------------------- # Set the Rust log level (e.g., info, debug) RUST_LOG=info
These variables will be picked up by the CLI, or you can supply equivalent
--rpc-url
,--ncn-program-id
,--ncn
,--keypair-path
, etc., flags to each command.
Initializing a New NCN Program
Before running the keeper, some setup and initialization steps are required to configure the NCN program and connect it. Below is a typical workflow for initializing a new NCN:
- Fund the Account Payer: The NCN program will create and maintain several temporary accounts (for snapshots, vote tracking, etc.). The program uses a payer account to pay rent for these accounts. You should fund this payer with some SOL to cover rent and fees. The CLI provides a command to transfer SOL from your keypair to the payer account:
ncn-program-cli admin-fund-account-payer --amount-in-sol 10
This example funds the account payer with 10 SOL.
- Create the NCN Config: Initialize the NCN program’s global configuration on-chain. This must be done by the NCN’s
ncn_admin
:
ncn-program-cli admin-create-config --tie-breaker-admin <ADMIN_ADDRESS>
This creates the NCN’s config account and sets an admin to resolve tied votes or set consensus manually, if needed. You can also override default consensus parameters with options like --epochs-before-stall
, --valid-slots-after-consensus
, etc., but in most cases defaults are fine. Run with --help
to see all available options.
- Create the Vault Registry: The Vault Registry is an on-chain account in the NCN program that will list all vaults (stake pools or restaked assets) participating in this particular NCN. Initialize it with:
ncn-program-cli create-vault-registry
This sets up an empty VaultRegistry account.
- Register Supported Tokens: Each vault that will contribute stake must be registered under a supported stake token with a weight. The VaultRegistry tracks supported mints and vaults, allowing the snapshot phase to identify which operators hold stake and calculate their voting power:
ncn-program-cli admin-register-st-mint --vault <VAULT_MINT_ADDRESS> --weight <WEIGHT_VALUE> --keypair-path <NCN_ADMIN_KEYPAIR>
For example, if you want to include a vault with mint ABC...
at weight 100, you’d put that address and weight. This call authorizes that vault for the NCN. Please note that the vault must have already been approved on the restaking program side via a handshake with this NCN.
Running the Keeper
The keeper
command automates key tasks for each epoch, including creating epoch state accounts, performing stake snapshots, and handling the voting process. It runs continuously while monitoring the blockchain and executing actions based on the current epoch phase.
To start the keeper, run:
ncn-program-cli keeper
By default, the keeper checks for actions every 10 minutes, retries on errors after 10 seconds, targets the testnet
cluster and reports metrics using the solana_metrics
crate with the local
region label.
Let’s break down the keeper’s workflow step by step.
1. Vault Registration
After registering the stake mints, you need to create entries in the Vault Registry for any vaults that have opted into the NCN. This is a permissionless crank operation: ncn-program-cli crank-register-vaults
.
crank_register_vaults
is a function that registers any unregistered vaults that have been approved by the NCN but not added to the registry yet. It will:
- Fetch all approved accounts
- Retrieve the current vault registry
- Identify the missing vaults by comparing approved vaults against already registered ones
- Register each missing vault individually
Once all eligible vaults are registered, the keeper continues its loop by checking and updating the current epoch state.
2. Fetch Epoch State
Next, the keeper then reads the current epoch from the Solana cluster using state.fetch(handler, current_keeper_epoch).await
and fetches the corresponding EpochState
account from the NCN program. If the account already exists, it loads it into local memory.
If the epoch has already been marked as complete, the keeper exits the loop early and waits for the next epoch.
3. Update Epoch state - Syncing local state with on-chain epoch data
The update_epoch_state
method ensures the keeper’s in-memory state reflects the latest on-chain data for the current epoch. It performs the following actions:
- Checks if the epoch is already completed using
get_is_epoch_completed
. If so, it flags the local state and exits early - Fetches the
EpochState
account - Validates the account data to make sure it is present and of the correct size.
- Deserializes the account data into an
EpochState
struct. - Updates the keeper’s memory with the deserialized state.
- Determines the current phase of the epoch by calling
update_current_state
.
This function acts as the gatekeeper. If the epoch is already finished, the keeper skips further processing for that loop iteration.
4. Core State Machine Operations
At this point in the loop, the keeper enters its core state machine execution phase, where it actively drives the NCN epoch forward based on its current on-chain state.
The NCN program defines a set of epoch phases. Each phase requires actions to be executed before the epoch can progress. The keeper reads the current EpochState
, determines the phase and runs the appropriate handler.
The epoch lifecycle states are:
SetWeight
→ Establishes voting weight structure for the epochSnapshot
→ Captures stake distribution across operatorsVote
→ This is skipped by the NCN keeperPostVoteCooldown
→ Manages post-consensus waiting periodDistribute
→ Distributes rewards to participants based on their contributionsClose
→ Cleans up completed epoch accounts
Each state represents a distinct phase in the epoch lifecycle and the keeper automatically transitions between states as on-chain conditions are met. These operations are permissionless meaning any keeper can execute them when the appropriate conditions are satisfied. It is important to note that this is an example of an NCN’s lifecycle. NCNs may have different states to crank through.
Let’s examine each state handler, starting with the weight setup phase:
SetWeight
The SetWeight state is the first operational phase of each epoch, responsible for establishing the voting power structure that will be used during consensus. This phase uses the function crank_set_weight
to set up the foundation for stake-weighted voting by creating and populating the weight table.
This function performs two steps:
create_weight_table
– Initializes and sizes theWeightTable
account- Depends on the total number of vaults in the registry
- Prepares a data structure to store weights efficiently on-chain
set_epoch_weights
– Calculates and stores each vault’s voting weight- Fetches the registered stake mints and their weights
- Calculates each vault’s total effective stake based on these weights
- Writes the results into the
WeightTable
account
Once voting weights are set, the epoch transitions to the Snapshot state, where the current stake distribution across all registered operators is captured.
Snapshot
The Snapshot phase records the current stake distribution across all vault-operator pairs for the epoch. This step guarantees a fixed, on-chain snapshot of delegated stake that will be used in the upcoming consensus vote.
The crank_snapshot
function performs several steps:
- Retrieve vaults and operators
- Fetches all valid vaults from the
VaultRegistry
- Fetches all registered operators in the NCN
- Fetches all valid vaults from the
- Skips if already finalized
- If the
EpochSnapshot
has already been finalized, the function exits early and moves on the next state
- If the
- Loop through each operator
- Makes sure an
OperatorSnapshot
exists for the current epoch - Filters vaults that have not yet been recorded in this snapshot
- Makes sure an
- Process vaults
- Calls
full_vault_update()
to update the vault’s state and stake balances - Calls
snapshot_vault_operator_delegation()
to record how much stake the vault has delegated to this operator
- Calls
This snapshot process creates a record of how much stake is delegated from each vault to each operator. It ensures that consensus voting in the next phase is based on accurate stake amounts.
Vote
This is skipped by the NCN while waiting for the operator to vote.
PostVoteCooldown
The PostVoteCooldown state serves as a buffer between finalizing consensus and performing cleanup. It gives the network time to settle and provides visibility into the outcome of the voting phase.
The crank_post_vote_cooldown
function performs two simple but important steps:
- Fetch Result: Loads the finalized
ConsensusResult
account for the epoch from the chain. - Log Outcome: Prints the result to the logs for debugging and audit purposes.
This phase does not submit any transactions or mutate state. It simply confirms that consensus has been reached and prepares the system for the final cleanup phase.
Once completed, the epoch transitions to the Close state, where all temporary accounts are cleaned up.
Distribute
The Distribute state allocates rewards to operators and vaults based on their contributions during the epoch.
The crank_distribute
function performs the following steps:
- Distribute NCN Rewards: Calls
distribute_ncn_rewards
to allocate base rewards tied to consensus participation. - Distribute Protocol Rewards: Invokes
distribute_jito_rewards
to distribute incentives. - Route NCN Receiver Rewards: If rewards exist for the reward receiver at the NCN-level, routes them using
route_ncn_rewards
. - Operator Vault Reward Routing: For each operator, it will set up their reward routing and distributes rewards to associated vaults.
- Distribute Operator Rewards: If an operator has accumulated rewards, it distributes them via
distribute_ncn_operator_rewards
. - Distribute Vault Rewards: Loops through each vault under the operator and distributes rewards via
distribute_ncn_vault_rewards
.
All reward distribution and routing steps are logged. Errors are non-blocking and distribution will be retried in future keeper loops if any step fails.
Once completed, the epoch moves to the Close
state, where the temporary accounts are cleaned up.
Close
The Close state marks the end of an NCN’s epoch lifecycle. During this phase, the keeper performs a full cleanup by closing all temporary accounts created during the epoch. This will reclaim rent, free up state, and prepare the NCN for the next epoch.
The crank_close_epoch_accounts
function performs the following operations:
- Close Ballot Box – Closes the
BallotBox
account that tracked consensus voting - Close Operator Snapshots – Iterates through each operator and closes their
OperatorSnapshot
account - Close Epoch Snapshot – Closes the global
EpochSnapshot
that captured the operator-vault stake mapping - Close Weight Table – Closes the
WeightTable
account that stored epoch voting weights - Close Epoch State – Closes the
EpochState
account that tracked progress through the state machine
Each closure is attempted independently and any errors are logged. Failures do not block anything. **The keeper will simply attempt to retry them in subsequent loops.
5. Timeout and Heartbeat
At the end of each loop, the keeper:
- Checks whether the epoch has stalled
- If a stall is detected and no actions remain, it waits for the
loop_timeout_ms
duration - Emits a heartbeat metric with the current tick count
- Starts the next iteration
This ensures the keeper remains responsive during stalled epochs while continuously reporting liveness for monitoring and reward tracking.
Operator
With NCNs, operators are responsible for driving consensus. While each operator can have its own unique logic, it’s up to the NCN designer to define that behavior. Operators perform all computation off-chain and submit votes on-chain during specific windows, using stake delegated by vaults. To simplify their responsibilities, the operator process automates the on-chain tasks for registered operators, primarily casting votes, handling post-vote logic, and reporting metrics. It runs continuously and monitors the state of the network and acts when it’s the operator’s turn to participate. In this guide, we’ll be looking at a template operator that fetches weather data and votes on the result.
This process is typically run by the same entity that registered the operator, such as a validator, DAO or data provider participating in the NCN.
This guide explains how to configure and run the operator using the ncn-operator-cli
from the NCN template. It breaks down the operator loop, details how votes are cast using real-world weather data and walks through the behavior during different epoch states like Vote
, PostVoteCooldown
, and Close
.
Installation and Setup
Before using the Template Operator CLI, install the necessary binaries:
-
Clone the repo
# Clone the template repo git clone [email protected]:jito-foundation/ncn-template.git cd ncn-template # Build the CLI from the repository (assuming you're in the repo directory) cargo build --release # Install the CLI binary cargo install --path ./cli --bin ncn-operator-cli --locked
After installation, verify it works by running:
ncn-operator-cli --help
-
Install Jito (Re)Staking CLI (if not already): The NCN program operates alongside Jito’s restaking program. You may need the Jito (Re)Staking CLI (
jito-restaking-cli
) to manage restaking registry tasks (like registering NCNs, operators, and vaults). Install it using Cargo:cargo install jito-restaking-cli
Confirm it works:
jito-restaking-cli --help
-
Configure Environment Variables: The
ncn-program-cli
accepts configuration through command-line flags or environment variables. Optionally, to avoid passing flags every time, you can use a.env
file for convenience:# Operator Environment Configuration # Copy this file to `.env` and update the values below # --------------- REQUIRED -------------------- # Solana cluster (mainnet, devnet, testnet, or localnet) CLUSTER=devnet # Solana RPC endpoint (must support getBlock and transaction history) RPC_URL=https://api.devnet.solana.com # Commitment level for operations (e.g. confirmed or finalized) COMMITMENT=confirmed # Your deployed NCN instance address NCN=<Your_NCN_account_address> # Path to your keypair file (admin/operator authority) KEYPAIR_PATH=~/.config/solana/id.json # Operator public key (the account that votes on-chain) OPERATOR=BSia35bXHZx69XzCQeMUnWqZJsUwJURVvuUg8Jup2BcP # OpenWeather API key for the example oracle operator OPENWEATHER_API_KEY=your_api_key_here # --------------- PROGRAM IDS -------------------- # Leave blank to use defaults unless you have custom deployments NCN_PROGRAM_ID==<Your_NCN_Program_ID> RESTAKING_PROGRAM_ID=RestkWeAVL8fRGgzhfeoqFhsqKRchg6aa1XrcH96z4Q VAULT_PROGRAM_ID=Vau1t6sLNxnzB7ZDsef8TLbPLfyZMYXH8WTNqUdm9g8 # --------------- LOGGING -------------------- # Set log level (info, debug, etc.) RUST_LOG=info
These variables will be picked up by the CLI, or you can supply equivalent
--rpc-url
,--ncn-program-id
,--ncn
, etc., flags to each command.
Running the Operator
The run-operator
command automates vote casting and post-vote actions for a registered operator. It runs continuously, monitoring the NCN’s epoch state and executing vote-related instructions when appropriate. It also emits metrics for visibility and debugging.
To start the operator, run:
ncn-program-cli run-operator
By default, the operator loop checks for actions every 10 minutes, retries on errors after 10 seconds, targets the testnet
cluster and reports metrics with the local
region label.
Let’s break down the operator’s workflow step by step.
1. Epoch Progression
Before doing any work, the operator checks whether a new epoch has started by querying the cluster by calling progress_epoch
if the epoch state is completed. This checks that the operator is aligned with the live on-chain epoch and doesn’t act on stale data.
The loop progresses through:
- Advancing to a new epoch if the chain has moved forward
- Looping back to the start of the same epoch if it’s marked complete
- Staying on the same epoch if work is still pending
2. Fetch or Update Operator State
The operator maintains an internal KeeperState
that tracks the current epoch, cached on-chain accounts and the latest EpochState
. This block loads the latest on-chain data to keep the operator aligned with the current epoch.
There are two possible paths here:
-
New Epoch Detected:
If the loop has progressed to a new epoch, it calls
state.fetch(...)
which does the following:- Sets the operator’s internal epoch value to the current one
- Loads all relevant on-chain accounts
- Calls
update_epoch_state(...)
internally to populate the latestEpochState
-
Same Epoch:
If the epoch hasn’t changed, it will skip the full fetch and just refresh the
EpochState
usingupdate_epoch_state(...)
This avoids unnecessary on-chain requests and helps keep everything responsive.
If either call fails, the operator logs the error and skips the current loop without submitting any vote or metrics.
3. Check for Valid EpochState
After updating its state, the operator then checks if a valid EpochState
exists.
If the EpochState
is missing or not yet initialized on-chain, the operator will:
- Log that the epoch has no associated state
- Mark the epoch as completed locally
- Skip to the next loop cycle
This prevents the operator from crashing or spinning unnecessarily while waiting for the epoch to be initialized.
4. Core State Machine Operations
Once the EpochState
is loaded, the operator identifies the current phase and reacts based on its role as an operator. Only a subset of phases require action.
It will evaluate internal conditions to determine eligibility. If the operator is permitted to vote in the current phase, it proceeds with the voting logic.
The epoch lifecycle states are:
SetWeight
→ Establishes voting weight structure for the epoch. No operator action is needed for this step.Snapshot
→ Captures stake distribution across operators. No operator action is needed for this step.Vote
→ Casts votePostVoteCooldown
→ Triggers post-vote logic and emits operator metrics. Marks the epoch as completed.Close
→ Cleans up completed epoch accounts
SetWeight
This step is skipped by the operator as no action is needed.
Snapshot
Again, this step is skipped by the operator.
Vote
The Vote
phase is where the operator performs its most important role: submitting a vote that contributes to the NCN’s consensus process. This phase is only active if the operator has received delegation from at least one vault and has not yet cast a vote for the current epoch.
During this phase, the operator:
-
Loads Required Data
It fetches both the
BallotBox
and theOperatorSnapshot
(which contains data about the operator’s delegation and voting history). These accounts determine whether the operator is eligible to vote and if they’ve already participated in this round. -
Checks Eligibility
Using
can_operator_vote(...)
, it will verify that the operator:- Has been delegated stake for this epoch
- Has not already voted
- Is listed in the ballot box with an appropriate weight
-
Casts the Vote
If eligible, the operator calls
operator_crank_vote(...)
to submit the vote on-chain. The actual vote content will be determined by the NCN’s logic. In the default template, it maps mock weather data to a vote value. In real NCNs, this would be replaced with your logic and inputs (e.g. price feeds, validator scores, etc.). -
Handles Errors
If voting fails, the operator logs the error, delays for the
--error-timeout-ms
and retries the loop. This prevents spammy retries and gives the network time to recover from short lived failures. -
Emits Metrics
Once successful, the operator emits the operator vote metrics using
emit_ncn_metrics_operator_vote(...)
. This helps monitor and track vote activity and operator performance in real time. -
Post-Vote Flow
If the operator has already voted or is ineligible:
- The operator instead performs a
post_vote
action which typically submits metadata or confirms the final state - It emits corresponding post-vote metrics
- Finally, it marks the epoch as complete for this operator and allows the operator to skip this epoch in future iterations
- The operator instead performs a
PostVoteCooldown
This phase is used to report the result of the voting process.
The operator:
- Loads the
BallotBox
- Checks whether consensus was reached
- Logs the outcome of the vote (including weights, operator decisions and winning ballot)
- Emits post-vote metrics
While no vote is cast, the operator may still submit an on-chain transaction (e.g. metrics or metadata), depending on the implementation.
Close
This phase is similar to PostVoteCooldown
, but is used at the very end of the epoch.
The operator once again:
- Loads the ballot box and logs the final consensus result
- Emits final metrics
- Marks the epoch as completed so the operator loop can progress to the next one
5. Timeout and Heartbeat
At the end of each loop, the operator:
- Waits for
-loop-timeout-ms
duration - Emits a heartbeat metric with the current tick count
- Starts the loop again
This helps avoid overloading the RPC and keeps the operator reporting liveness for monitoring dashboards, alerting systems, and reward eligibility checks.
Core struct definitions
Here are the definitions for the core data structures used by the NCN program, typically found in the /core/src
directory. Understanding these structures is key to understanding the program’s logic.
Config
file: config.rs
- Purpose: Stores global, long-lived configuration parameters for the NCN program instance.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct Config {
/// The Pubkey of the associated Jito Restaking NCN account this config belongs to.
pub ncn: Pubkey,
/// The admin authorized to update the tie breaker mechanism or parameters.
pub tie_breaker_admin: Pubkey,
/// Number of slots after consensus is reached where votes are still accepted
/// (though they won't change the outcome).
pub valid_slots_after_consensus: PodU64,
/// Number of epochs without reaching consensus before the cycle is considered stalled.
pub epochs_before_stall: PodU64,
/// Number of epochs to wait after consensus is reached before epoch accounts can be closed.
pub epochs_after_consensus_before_close: PodU64,
/// The first epoch number for which voting is considered valid.
pub starting_valid_epoch: PodU64,
/// Bump seed for the PDA
pub bump: u8,
}
- Explanation: Holds the associated
ncn
, thetie_breaker_admin
, and various timing/threshold parameters (valid_slots_after_consensus
,epochs_before_stall
,epochs_after_consensus_before_close
,starting_valid_epoch
).
Ballot
file: ballot_box.rs
- Purpose: Represents a single potential outcome in the consensus process, specifically a weather status in this example.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct Ballot {
/// The weather status value
weather_status: u8,
/// Whether the ballot is valid
is_valid: PodBool,
}
- Explanation: Holds the numeric
weather_status
being voted on and a booleanis_valid
flag to ensure it corresponds to a known status.
BallotTally
file: ballot_box.rs
- Purpose: Aggregates votes and stake weight for a specific
Ballot
. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct BallotTally {
/// Index of the tally within the ballot_tallies
index: PodU16,
/// The ballot being tallied
ballot: Ballot,
/// Breakdown of all of the stake weights that contribute to the vote
stake_weights: StakeWeights,
/// The number of votes for this ballot
tally: PodU64,
}
- Explanation: Tracks which
ballot
this tally is for, itsindex
in the main array, the totalstake_weights
supporting it, and the rawtally
(count) of votes.
OperatorVote
file: ballot_box.rs
- Purpose: Records the vote cast by a single operator within a specific epoch’s
BallotBox
. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct OperatorVote {
/// The operator that cast the vote
operator: Pubkey,
/// The slot when the operator voted
slot_voted: PodU64,
/// The stake weights of the operator
stake_weights: StakeWeights,
/// The index of the ballot in the ballot_tallies array
ballot_index: PodU16,
}
- Explanation: Stores the
operator
pubkey, the currentslot
, theirstake_weights
, and theballot_index
they voted for.
BallotBox
file: ballot_box.rs
- Purpose: The central account for managing the voting process within a specific epoch.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct BallotBox {
/// The Pubkey of the NCN this ballot box is for
ncn: Pubkey,
/// The epoch this ballot box is for
epoch: PodU64,
/// Bump seed for the PDA
bump: u8,
/// Slot when this ballot box was created
slot_created: PodU64,
/// Slot when consensus was reached
slot_consensus_reached: PodU64,
/// Number of operators that have voted
operators_voted: PodU64,
/// Number of unique ballots
unique_ballots: PodU64,
/// The ballot that got at least 66% of votes
winning_ballot: Ballot,
/// Operator votes
operator_votes: [OperatorVote; MAX_OPERATORS],
/// Mapping of ballots votes to stake weight
ballot_tallies: [BallotTally; MAX_OPERATORS],
}
- Explanation: Holds metadata (
ncn
,epoch
, timestamps), vote counts, and arrays for individual operator votes and aggregated tallies.
ConsensusResult
file: consensus_result.rs
- Purpose: A persistent account storing the final, immutable outcome of a consensus cycle for a specific epoch.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct ConsensusResult {
/// The Pubkey of the NCN this consensus result is for
ncn: Pubkey,
/// The epoch this consensus result is for
epoch: PodU64,
/// The vote weight that supported the winning status
vote_weight: PodU64,
/// The total vote weight in the ballot box
total_vote_weight: PodU64,
/// The slot at which consensus was reached
consensus_slot: PodU64,
/// Bump seed for the PDA
bump: u8,
/// The winning weather status that reached consensus
weather_status: u8,
}
- Explanation: Stores the
ncn
,epoch
, the winningweather_status
, and theconsensus_slot
.
AccountPayer
file: account_payer.rs
- Purpose: An empty, uninitialized system account used solely as a Program Derived Address (PDA) to hold SOL temporarily for paying rent during account creation or reallocation within the NCN program.
- Definition:
pub struct AccountPayer {}
- Explanation: This is a marker struct with no fields. Its associated functions handle deriving the PDA and performing SOL transfers for rent payments using
invoke_signed
.
EpochMarker
file: epoch_marker.rs
- Purpose: An empty account created as a marker to signify that all temporary accounts associated with a specific NCN epoch have been successfully closed.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct EpochMarker {
ncn: Pubkey,
epoch: PodU64,
slot_closed: PodU64,
}
- Explanation: Contains the
ncn
, theepoch
that was closed, and theslot_closed
. Its existence confirms cleanup completion for that epoch.
EpochSnapshot
file: epoch_snapshot.rs
- Purpose: Captures the aggregated state of the NCN system at the beginning of a specific epoch snapshot phase.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct EpochSnapshot {
/// The Pubkey of the NCN this snapshot is for
ncn: Pubkey,
/// The epoch this snapshot is for
epoch: PodU64,
/// Bump seed for the PDA
bump: u8,
/// Slot when this EpochSnapshot account was created
slot_created: PodU64,
/// Slot when the snapshotting process (including all operator delegations) was completed
slot_finalized: PodU64,
/// Number of operators in the epoch
operator_count: PodU64,
/// Number of vaults in the epoch
vault_count: PodU64,
/// Keeps track of the number of completed operator registration through `snapshot_vault_operator_delegation` and `initialize_operator_snapshot`
operators_registered: PodU64,
/// Keeps track of the number of valid operator vault delegations
valid_operator_vault_delegations: PodU64,
/// Tallies the total stake weights for all vault operator delegations
stake_weights: StakeWeights,
}
- Explanation: Stores metadata (
ncn
,epoch
, timestamps), counts (operator_count
,vault_count
), progress trackers, and the total aggregatedstake_weights
for the epoch.
OperatorSnapshot
file: epoch_snapshot.rs
- Purpose: Captures the state of a single operator for a specific epoch, including their total delegated stake weight and a breakdown of contributions from each vault.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct OperatorSnapshot {
operator: Pubkey,
ncn: Pubkey,
ncn_epoch: PodU64,
bump: u8,
slot_created: PodU64,
slot_finalized: PodU64,
is_active: PodBool,
ncn_operator_index: PodU64,
operator_index: PodU64,
operator_fee_bps: PodU16,
vault_operator_delegation_count: PodU64,
vault_operator_delegations_registered: PodU64,
valid_operator_vault_delegations: PodU64,
stake_weights: StakeWeights,
vault_operator_stake_weight: [VaultOperatorStakeWeight; MAX_VAULTS],
}
- Explanation: Contains operator/NCN identifiers, timestamps, status, indices,
operator_fee_bps
, delegation counts/progress, the operator’s totalstake_weights
, and a detailed breakdown invault_operator_stake_weight
.
VaultOperatorStakeWeight
file: epoch_snapshot.rs
- Purpose: A helper struct within
OperatorSnapshot
to store the calculated stake weight originating from one specific vault’s delegation to that operator. - Definition:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Zeroable, Pod)]
pub struct VaultOperatorStakeWeight {
vault: Pubkey,
vault_index: PodU64,
stake_weight: StakeWeights,
}
- Explanation: Links a
vault
pubkey andvault_index
to the specificstake_weight
derived from its delegation to the parentOperatorSnapshot
.
StMintEntry
file: vault_registry.rs
- Purpose: Represents a supported token mint within the
VaultRegistry
, storing its address and associated voting weight. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct StMintEntry {
/// The supported token ( ST ) mint
st_mint: Pubkey,
// Either a switchboard feed or a weight must be set
/// The switchboard feed for the mint
reserve_switchboard_feed: [u8; 32],
/// The weight
weight: PodU128,
}
- Explanation: Stores the
st_mint
address and its assigned votingweight
.reserve_switchboard_feed
is unused here.
VaultEntry
file: vault_registry.rs
- Purpose: Represents a registered vault within the
VaultRegistry
. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct VaultEntry {
/// The vault account
vault: Pubkey,
/// The supported token ( ST ) mint of the vault
st_mint: Pubkey,
/// The index of the vault in respect to the NCN account
vault_index: PodU64,
/// The slot the vault was registered
slot_registered: PodU64,
}
- Explanation: Stores the
vault
address, thest_mint
it holds, its assignedvault_index
, and theslot_registered
.
VaultRegistry
file: vault_registry.rs
- Purpose: A global account for the NCN program instance that maintains the list of all supported token mints (
StMintEntry
) and all registered vaults (VaultEntry
). - Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct VaultRegistry {
/// The Pubkey of the associated NCN account this registry belongs to.
pub ncn: Pubkey,
/// Bump seed for the PDA
pub bump: u8,
/// Array storing entries for each supported token mint
pub st_mint_list: [StMintEntry; MAX_ST_MINTS],
/// Array storing entries for each vault
pub vault_list: [VaultEntry; MAX_VAULTS],
}
- Explanation: Holds the
ncn
identifier,bump
, and arrays forst_mint_list
andvault_list
.
WeightTable
file: weight_table.rs
- Purpose: An epoch-specific account that snapshots the weights of all supported tokens at the beginning of the epoch.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct WeightTable {
/// The Pubkey of the associated NCN account this account is for.
ncn: Pubkey,
/// The epoch this account is for.
epoch: PodU64,
/// Slot when this WeightTable account was created.
slot_created: PodU64,
/// Number of vaults in tracked mints at the time of creation
vault_count: PodU64,
/// Bump seed for the PDA
bump: u8,
/// A snapshot copy of the relevant vault entries from the VaultRegistry
vault_registry: [VaultEntry; MAX_VAULTS],
/// The weight table
table: [WeightEntry; MAX_ST_MINTS],
}
- Explanation: Contains metadata (
ncn
,epoch
,slot_created
,vault_count
), a snapshot of thevault_registry
, and the maintable
holdingWeightEntry
structs with the frozen weights for the epoch.
EpochAccountStatus
file: epoch_state.rs
- Purpose: A helper struct within
EpochState
used to track the lifecycle status of various temporary accounts associated with a specific epoch. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, ShankType, Pod)]
#[repr(C)]
pub struct EpochAccountStatus {
/// Status of the main EpochState account itself.
epoch_state: u8,
/// Status of the WeightTable account for this epoch.
weight_table: u8,
/// Status of the main EpochSnapshot account for this epoch.
epoch_snapshot: u8,
/// Status array for each individual OperatorSnapshot account.
operator_snapshot: [u8; MAX_OPERATORS],
/// Status of the BallotBox account for this epoch.
ballot_box: u8,
}
- Explanation: Uses
u8
fields to represent the status of various temporary accounts associated with a specific epoch.
NCNRewardRouter
file: ncn_reward_router.rs
- Purpose: The main entry point for routing rewards from NCNs. This router receives rewards and distributes them according to the fee structure.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct NCNRewardRouter {
/// NCN the account is associated with
ncn: Pubkey,
/// The epoch the account is associated with
epoch: PodU64,
/// Bump seed for the PDA
bump: u8,
/// Slot the account was created
slot_created: PodU64,
/// Total rewards routed (in lamports) - cumulative amount ever processed
total_rewards: PodU64,
/// Amount of rewards in the reward pool (in lamports) - awaiting distribution
reward_pool: PodU64,
/// Amount of rewards processed (in lamports) - moved out of reward pool for distribution
rewards_processed: PodU64,
/// Reserved space for future fields
reserved: [u8; 128],
/// Last vote index processed during routing (for resuming partial operations)
last_vote_index: PodU16,
/// Last rewards amount being processed during routing (for resuming partial operations)
last_rewards_to_process: PodU64,
/// Rewards allocated to the Protocol (ready for distribution)
protocol_rewards: PodU64,
/// Rewards allocated to the NCN (ready for distribution)
ncn_rewards: PodU64,
/// Total rewards allocated to operator-vault reward receivers (before individual routing)
operator_vault_rewards: PodU64,
/// Individual operator reward routes - tracks rewards per operator
/// Array size 256 limits the number of operators that can participate in an epoch
operator_vault_reward_routes: [OperatorVaultRewardRoute; 256],
}
- Explanation: The router distributes rewards in three tiers: 4% to Protocol, 4% to NCN, and 92% to operator-vault rewards. It supports partial routing through iterations to handle large numbers of operators without hitting transaction limits.
OperatorVaultRewardRouter
file: operator_vault_reward_router.rs
- Purpose: Routes rewards from operators to their associated vaults. This router handles the final stage of reward distribution where operator rewards are further distributed to the vaults they operate.
- Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct OperatorVaultRewardRouter {
/// The operator the router is associated with
operator: Pubkey,
/// The NCN the router is associated with
ncn: Pubkey,
/// The epoch the router is associated with
epoch: PodU64,
/// The bump seed for the PDA
bump: u8,
/// The slot the router was created
slot_created: PodU64,
/// The operator's index within the NCN
ncn_operator_index: PodU64,
/// The total rewards that have been routed (in lamports) - cumulative amount ever processed
total_rewards: PodU64,
/// The rewards in the reward pool (in lamports) - awaiting distribution
reward_pool: PodU64,
/// The rewards that have been processed (in lamports) - moved out of reward pool
rewards_processed: PodU64,
/// Rewards allocated to the operator (in lamports) - operator's fee portion
operator_rewards: PodU64,
/// The last rewards amount being processed during routing (for resuming partial operations)
last_rewards_to_process: PodU64,
/// The last vault operator delegation index processed during routing
last_vault_operator_delegation_index: PodU16,
/// Individual vault reward routes - tracks rewards per vault (limited to 64 vaults)
vault_reward_routes: [VaultRewardRoute; 64],
}
- Explanation: The distribution is based on the operator taking their fee percentage first, then remaining rewards are distributed to vaults proportionally by stake weight. It supports partial routing through iterations to handle large numbers of vaults.
OperatorVaultRewardRoute
file: ncn_reward_router.rs
- Purpose: A component structure within
NCNRewardRouter
that tracks rewards allocated to a specific operator within the reward routing system. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, ShankType)]
#[repr(C)]
pub struct OperatorVaultRewardRoute {
/// The operator pubkey
operator: Pubkey,
/// Reward amount allocated to this operator
rewards: NCNRewardRouterRewards,
}
- Explanation: Stores the mapping between an operator and their allocated reward amount within the NCN reward routing system.
VaultRewardRoute
file: operator_vault_reward_router.rs
- Purpose: A component structure within
OperatorVaultRewardRouter
that tracks rewards allocated to a specific vault within the operator’s reward distribution. - Definition:
#[derive(Debug, Clone, Copy, Zeroable, Pod, ShankType)]
#[repr(C)]
pub struct VaultRewardRoute {
/// The vault pubkey that will receive rewards
vault: Pubkey,
/// The amount of rewards allocated to this vault (in lamports)
rewards: PodU64,
}
- Explanation: Stores the mapping between a vault and its allocated reward amount within an operator’s reward distribution system.