- 🎯 Core Purpose
- 🏛️ Architecture Overview
- 🔧 Core Components Deep Dive
- 🧮 BN128 Mathematics and Cryptographic Foundations
- 🔄 Consensus Workflow
- 🛠️ CLI Tools & Automation
- 🧪 Testing Infrastructure
- 🏗️ Local Test Validator
- 🔧 Development Workflow
- 🚀 Deployment Guide
- 🔐 Security Considerations
- 📈 Performance & Scalability
- The command that you need to run to get started
This program serves as a template for building decentralized consensus networks where multiple operators can collectively agree on shared state through:
- BLS signature aggregation: Efficient cryptographic proof of consensus
Version: 0.0.1
Last Updated: August 2024
- ✅ Simplified instruction set from 22 to 16 instructions
- ✅ Removed weight table system (single vault optimization)
- ✅ Enhanced BLS signature aggregation with anti-rogue key protection
- ✅ Improved CLI with multi-signature aggregation support
- ✅ Added comprehensive fuzz testing for consensus scenarios
- ✅ Streamlined snapshot system architecture
┌─────────────────────────────────────────────────────────────┐
│ Jito Restaking Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ NCN Program (Core Consensus Logic) │
│ ├── Config Management │
│ ├── Registry Management (Vaults & Operators) │
│ ├── Epoch State Management │
│ ├── Snapshot System │
│ ├── BLS Voting System │
│ └── Account Lifecycle Management │
├─────────────────────────────────────────────────────────────┤
│ Client Libraries │
│ ├── Rust Client (Auto-generated) │
│ └── JavaScript Client (Auto-generated) │
├─────────────────────────────────────────────────────────────┤
│ CLI Tools │
│ ├── NCN Program CLI │
│ └── Keepr │
├─────────────────────────────────────────────────────────────┤
│ Testing & Integration │
│ ├── Unit Tests │
│ ├── Integration Tests │
│ └── Simulation Tests │
└─────────────────────────────────────────────────────────────┘
ncn-program-template-bls/
├── 📋 Program Core
│ ├── program/ # Main Solana program entry point
│ │ ├── src/lib.rs # 16 instruction handlers (initialize→vote→close)
│ │ └── src/*.rs # Individual instruction implementations
│ └── core/ # Shared core functionality
│ ├── src/lib.rs # 20+ core modules (crypto, accounts, utils)
│ ├── schemes/ # BLS signature schemes (SHA256, normalized)
│ ├── g1_point.rs # G1 elliptic curve operations
│ ├── g2_point.rs # G2 elliptic curve operations
│ └── error.rs # 64 custom error types
│
├── 🔧 Client SDKs
│ ├── clients/rust/ # Auto-generated Rust client
│ │ └── ncn_program/src/ # Generated account/instruction types
│ └── clients/js/ # Auto-generated JavaScript client
│ └── ncn_program/ # TypeScript definitions & helpers
│
├── 🛠️ CLI Tools
│ └── cli/ # Comprehensive CLI tooling
│ ├── src/instructions.rs # CLI instruction wrappers
│ ├── getting_started.md # CLI usage documentation
│ └── api-docs.md # Complete API reference
│
├── 🧪 Testing Infrastructure
│ └── integration_tests/
│ ├── tests/fixtures/ # Test fixtures and programs
│ ├── tests/ncn_program/ # 16+ comprehensive test modules
│ └── src/main.rs # Test harness entry point
│
├── ⚙️ Configuration & Scripts
│ ├── .cargo/config.toml # Program ID environment variables
│ ├── scripts/generate-clients.js # Client generation automation
│ ├── format.sh # Code formatting and testing pipeline
│ ├── generate_client.sh # Quick client regeneration
│ └── idl/ncn_program.json # Interface definition (1746 lines)
│
└── 📚 Documentation
├── README.md # This comprehensive guide
├── cli/getting_started.md # CLI quick start guide
└── cli/api-docs.md # Complete API documentation
InitializeConfig
: Creates program configuration with consensus parametersInitializeVaultRegistry
: Sets up vault tracking systemRegisterVault
: Adds vaults to the registry (permissionless after handshake)RegisterOperator
: Adds operators with BLS public keysUpdateOperatorBN128Keys
: Updates operator cryptographic keysInitializeVoteCounter
: Creates vote counter for replay attack preventionInitializeSnapshot
: Creates immutable epoch state snapshotReallocSnapshot
: Expands snapshot storageInitializeOperatorSnapshot
: Captures individual operator state
CastVote
: Submits BLS aggregated signatures for consensus (uses counter for message)SnapshotVaultOperatorDelegation
: Records delegation relationships
AdminSetParameters
: Updates consensus parametersAdminSetNewAdmin
: Changes administrative rolesAdminRegisterStMint
: Adds supported stake token mints
pub struct Config {
ncn: Pubkey, // NCN identifier
tie_breaker_admin: Pubkey, // Admin for tie-breaking votes
valid_slots_after_consensus: PodU64, // Voting window after consensus
epochs_before_stall: PodU64, // Epochs before system stalls
epochs_after_consensus_before_close: PodU64, // Cleanup timing
starting_valid_epoch: PodU64, // First valid epoch
fee_config: FeeConfig, // Fee distribution settings
minimum_stake: StakeWeights, // Minimum participation threshold
}
pub struct Snapshot {
ncn: Pubkey, // NCN reference
epoch: PodU64, // Epoch number
operator_count: PodU64, // Total operators
operators_registered: PodU64, // Active operators
operators_can_vote_count: PodU64, // Eligible voters
total_aggregated_g1_pubkey: [u8; 32], // Aggregated public key
operator_snapshots: [OperatorSnapshot; 256], // Operator states
minimum_stake: StakeWeights, // Participation threshold
}
pub struct VaultRegistry {
ncn: Pubkey, // NCN reference
st_mint_list: [StMintEntry; 1], // Supported stake tokens
vault_list: [VaultEntry; 1], // Registered vaults
}
pub struct VoteCounter {
ncn: Pubkey, // NCN reference
count: PodU64, // Current vote counter
bump: u8, // PDA bump seed
reserved: [u8; 7], // Reserved for future use
}
The vote counter tracks the number of successful votes and provides automatic replay attack protection by using the counter value as the message for BLS signature verification.
The vote counter is a critical security component that prevents replay attacks by ensuring each vote uses a unique, sequential message:
// Vote counter provides the message for signature verification
let current_count = vote_counter.count();
let message = current_count.to_le_bytes(); // Padded to 32 bytes
- Sequential Uniqueness: Each vote increments the counter, making old signatures invalid
- Deterministic: No external dependencies - counter value is the message
- Atomic Updates: Counter only increments after successful signature verification
- Replay Prevention: Previous signatures cannot be reused due to counter advancement
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Read Counter │ -> │ Sign Counter │ -> │ Verify & Update │
│ (N = current) │ │ (Message = N) │ │ (N = N + 1) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- No Message Collisions: Sequential counter ensures unique messages
- Automatic Protection: No manual nonce management required
- Resistant to Attacks: Replay, precomputation, and signature reuse attacks are prevented
- Simple Verification: Anyone can verify counter progression
The system uses the BN254 (alt_bn128) elliptic curve for BLS signatures:
- G1 Points: 32-byte compressed format for signatures
- G2 Points: 64-byte compressed format for public keys
- Pairing Operations: Verification through bilinear pairings
Located in core/src/schemes/
:
Sha256Normalized
: Standard SHA-256 based message hashingtraits.rs
: Generic signing/verification interfaces
- Private keys: 32-byte scalars for signature generation
- G1 Public keys: For signature aggregation
- G2 Public keys: For verification operations
- Signature verification: Uses Solana's alt_bn128 precompiles
This section details the mathematical foundations underlying the BLS signature aggregation system implemented in the NCN program. The system uses the BN254 (alt_bn128) elliptic curve to enable efficient signature aggregation and verification through bilinear pairings.
The BN254 curve is defined over a finite field with the equation:
y² = x³ + 3 (mod p)
Where:
p = 21888242871839275222246405745257275088696311157297823662689037894645226208583
(prime modulus)- G1: Points on the base curve over Fp
- G2: Points on the twisted curve over Fp²
G1 Generator Point:
pub const G1_GENERATOR: [u8; 64] = [
// x coordinate: 1
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
// y coordinate: 2
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
];
Curve Order (MODULUS):
pub static MODULUS: UBig = unsafe {
UBig::from_static_words(&[
0x3c208c16d87cfd47,
0x97816a916871ca8d,
0xb85045b68181585d,
0x30644e72e131a029,
])
};
The system implements a deterministic hash-to-curve function using SHA-256 with normalization:
impl HashToCurve for Sha256Normalized {
fn try_hash_to_curve<T: AsRef<[u8]>>(message: T) -> Result<G1Point, NCNProgramError> {
(0..255)
.find_map(|n: u8| {
// Create a hash
let hash = solana_nostd_sha256::hashv(&[message.as_ref(), &[n]]);
// Convert hash to a Ubig for Bigint operations
let hash_ubig = UBig::from_be_bytes(&hash);
// Check if the hash is higher than our normalization modulus of Fq * 5
if hash_ubig >= NORMALIZE_MODULUS {
return None;
}
let modulus_ubig = hash_ubig % &MODULUS;
// Decompress the point
match alt_bn128_g1_decompress(&modulus_ubig.to_be_bytes()) {
Ok(p) => Some(G1Point(p)),
Err(_) => None,
}
})
.ok_or(NCNProgramError::HashToCurveError)
}
}
Mathematical Process:
- Domain Separation:
H(message || n)
wheren
is a counter (0-254) - Normalization: Ensure hash <
NORMALIZE_MODULUS
to avoid bias - Modular Reduction:
hash mod p
to get field element - Curve Mapping: Attempt to decompress as G1 point until valid point found
The system implements two verification modes: single signature and aggregated signature verification.
Single Signature Verification:
The verification equation is:
e(H(m), PK2) = e(σ, G2_GENERATOR)
Which is implemented as:
e(H(m), PK2) * e(σ, -G2_GENERATOR) = 1
where:
H(m)
is the hash of the messagePK2
is the G2 public keyσ
is the signatureG2_GENERATOR
is the generator point for the G2 curvee
is the pairing function
pub fn verify_signature<H: HashToCurve, T: AsRef<[u8]>, S: BLSSignature>(
self,
signature: S,
message: T,
) -> Result<(), NCNProgramError> {
let mut input = [0u8; 384];
// 1) Hash message to curve
input[..64].clone_from_slice(&H::try_hash_to_curve(message)?.0);
// 2) Decompress our public key
input[64..192].clone_from_slice(&self.0);
// 3) Decompress our signature
input[192..256].clone_from_slice(&signature.to_bytes()?);
// 4) Pair with -G2::one()
input[256..].clone_from_slice(&G2_MINUS_ONE);
// Calculate result
if let Ok(r) = alt_bn128_pairing(&input) {
msg!("Pairing result: {:?}", r);
if r.eq(&BN128_ADDITION_SUCESS_RESULT) {
Ok(())
} else {
Err(NCNProgramError::BLSVerificationError)
}
} else {
Err(NCNProgramError::AltBN128PairingError)
}
}
The aggregated signature verification uses a sophisticated scheme to prevent rogue key attacks:
Core Equation:
e(H(m) + α·G1, APK2) = e(σ + α·APK1, G2_GENERATOR)
Implemented as:
e(H(m) + α·G1, APK2) * e(σ + α·APK1, -G2_GENERATOR) = 1
Where:
α = H(H(m) || σ || APK1 || APK2)
(anti-rogue key factor)APK1 = Σ(PK1_i)
(aggregated G1 public keys)APK2
= aggregated G2 public keyσ
= aggregated signatureG2_GENERATOR
= generator point for the G2 curvee
= pairing function
pub fn verify_aggregated_signature<H: HashToCurve, T: AsRef<[u8]>, S: BLSSignature>(
self,
aggregated_signature: G1Point,
message: T,
apk1: G1Point,
) -> Result<(), NCNProgramError> {
let message_hash = H::try_hash_to_curve(message)?.0;
let alpha = compute_alpha(&message_hash, &aggregated_signature.0, &apk1.0, &self.0);
let scaled_g1 = G1Point::from(G1_GENERATOR).mul(alpha)?;
let scaled_aggregated_g1 = apk1.mul(alpha)?;
let msg_hash_plus_g1 = G1Point::from(message_hash) + scaled_g1;
let aggregated_signature_plus_aggregated_g1 = aggregated_signature + scaled_aggregated_g1;
let mut input = [0u8; 384];
// Pairing equation is:
// e(H(m) + G1_Generator * alpha, aggregated_g2) = e(aggregated_signature + aggregated_g1 * alpha, G2_MINUS_ONE)
// 1) Hash message to curve
input[..64].clone_from_slice(&msg_hash_plus_g1.0);
// 2) Decompress our public key
input[64..192].clone_from_slice(&self.0);
// 3) Decompress our signature
input[192..256].clone_from_slice(&aggregated_signature_plus_aggregated_g1.0);
// 4) Pair with -G2::one()
input[256..].clone_from_slice(&G2_MINUS_ONE);
// Calculate result
if let Ok(r) = alt_bn128_pairing(&input) {
msg!("Pairing result: {:?}", r);
if r.eq(&BN128_ADDITION_SUCESS_RESULT) {
Ok(())
} else {
Err(NCNProgramError::BLSVerificationError)
}
} else {
Err(NCNProgramError::AltBN128PairingError)
}
}
Anti-Rogue Key Factor Computation:
pub fn compute_alpha(
message: &[u8; 64],
signature: &[u8; 64],
apk1: &[u8; 64],
apk2: &[u8; 128],
) -> [u8; 32] {
// Concatenate all inputs
let mut input = Vec::with_capacity(message.len() + signature.len() + apk1.len() + apk2.len());
input.extend_from_slice(message);
input.extend_from_slice(signature);
input.extend_from_slice(apk1);
input.extend_from_slice(apk2);
// Hash the concatenated input
let hash = solana_nostd_sha256::hashv(&[&input]);
// Convert hash to UBig and reduce modulo MODULUS
let hash_ubig = UBig::from_be_bytes(&hash) % MODULUS.clone();
let mut alpha_bytes = [0u8; 32];
let hash_bytes = hash_ubig.to_be_bytes();
// Copy to 32 bytes, pad with zeros if needed
let pad = 32usize.saturating_sub(hash_bytes.len());
if pad > 0 {
alpha_bytes[..pad].fill(0);
alpha_bytes[pad..].copy_from_slice(&hash_bytes);
} else {
alpha_bytes.copy_from_slice(&hash_bytes[hash_bytes.len() - 32..]);
}
alpha_bytes
}
In the cast_vote
instruction, the system handles partial signature aggregation:
// Aggregate the G1 public keys of operators who signed
let mut aggregated_nonsigners_pubkey: Option<G1Point> = None;
let mut non_signers_count: u64 = 0;
for (i, operator_snapshot) in snapshot.operator_snapshots().iter().enumerate() {
if i >= operator_count as usize {
break;
}
let byte_index = i / 8;
let bit_index = i % 8;
let signed = (operators_signature_bitmap[byte_index] >> bit_index) & 1 == 1;
if signed {
let snapshot_epoch =
get_epoch(operator_snapshot.last_snapshot_slot(), ncn_epoch_length)?;
let current_epoch = get_epoch(current_slot, ncn_epoch_length)?;
let has_minimum_stake =
operator_snapshot.has_minimum_stake_now(current_epoch, snapshot_epoch)?;
if !has_minimum_stake {
msg!(
"The operator {} does not have enough stake to vote",
operator_snapshot.operator()
);
return Err(NCNProgramError::OperatorHasNoMinimumStake.into());
}
} else {
// Convert bytes to G1Point
let g1_compressed = G1CompressedPoint::from(operator_snapshot.g1_pubkey());
let g1_point = G1Point::try_from(&g1_compressed)
.map_err(|_| NCNProgramError::G1PointDecompressionError)?;
if aggregated_nonsigners_pubkey.is_none() {
aggregated_nonsigners_pubkey = Some(g1_point);
} else {
// Add this G1 pubkey to the aggregate using G1Point addition
let current = aggregated_nonsigners_pubkey.unwrap();
aggregated_nonsigners_pubkey = Some(
current
.checked_add(&g1_point)
.ok_or(NCNProgramError::AltBN128AddError)?,
);
}
non_signers_count = non_signers_count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?
}
}
When not all operators sign, the system computes the effective aggregate public key:
let total_aggregate_g1_pubkey_compressed =
G1CompressedPoint::from(snapshot.total_aggregated_g1_pubkey());
let total_aggregated_g1_pubkey = G1Point::try_from(&total_aggregate_g1_pubkey_compressed)
.map_err(|_| NCNProgramError::G1PointDecompressionError)?;
let signature_compressed = G1CompressedPoint(aggregated_signature);
let signature = G1Point::try_from(&signature_compressed)
.map_err(|_| NCNProgramError::G1PointDecompressionError)?;
// If there are no non-signers, we should verify the aggregate signature with the total G1
// pubkey because adding to the initial non-signers pubkey would result in error since it is
// initialized to all zeros and this is not a valid point of the curve BN128
if non_signers_count == 0 {
msg!("All operators signed, verifying aggregate signature with total G1 pubkey");
aggregated_g2_point
.verify_aggregated_signature::<Sha256Normalized, &[u8], G1Point>(
signature,
&message_32,
total_aggregated_g1_pubkey,
)
.map_err(|_| NCNProgramError::SignatureVerificationFailed)?;
} else {
msg!("Total non signers: {}", non_signers_count);
let aggregated_nonsigners_pubkey =
aggregated_nonsigners_pubkey.ok_or(NCNProgramError::NoNonSignersAggregatedPubkey)?;
let apk1 = total_aggregated_g1_pubkey
.checked_add(&aggregated_nonsigners_pubkey.negate())
.ok_or(NCNProgramError::AltBN128AddError)?;
msg!("Aggregated non-signers G1 pubkey {:?}", apk1.0);
msg!("Aggregated G2 pubkey {:?}", aggregated_g2_point.0);
// One Pairing attempt
msg!("Verifying aggregate signature one pairing");
aggregated_g2_point
.verify_aggregated_signature::<Sha256Normalized, &[u8], G1Point>(
signature,
&message_32,
apk1,
)
.map_err(|_| NCNProgramError::SignatureVerificationFailed)?;
}
Mathematical Logic:
- If all operators sign:
APK1 = Σ(PK1_i)
for all i - If some don't sign:
APK1 = Σ(PK1_i) - Σ(PK1_j)
where j are non-signers - This is computed as:
APK1 = total_aggregated_g1_pubkey + (-aggregated_nonsigners_pubkey)
The point negation operation is crucial for signature aggregation:
/// Returns the negation of the point: (x, -y mod p)
pub fn negate(&self) -> Self {
// x: first 32 bytes, y: last 32 bytes
let x_bytes = &self.0[0..32];
let y_bytes = &self.0[32..64];
let y = UBig::from_be_bytes(y_bytes);
let neg_y = if y == UBig::ZERO {
UBig::ZERO
} else {
(MODULUS.clone() - y) % MODULUS.clone()
};
let mut neg_point = [0u8; 64];
neg_point[0..32].copy_from_slice(x_bytes);
let neg_y_bytes = neg_y.to_be_bytes();
// pad to 32 bytes if needed
let pad = 32 - neg_y_bytes.len();
if pad > 0 {
for i in 0..pad {
neg_point[32 + i] = 0;
}
neg_point[32 + pad..64].copy_from_slice(&neg_y_bytes);
} else {
neg_point[32..64].copy_from_slice(&neg_y_bytes);
}
G1Point(neg_point)
}
The system uses a vote counter as the message:
// Get the current counter value to use as the message for signature verification
let vote_counter_data = vote_counter.data.borrow();
let vote_counter_account = VoteCounter::try_from_slice_unchecked(&vote_counter_data)?;
let current_count = vote_counter_account.count();
let message = current_count.to_le_bytes();
// Pad to 32 bytes for signature verification
let mut message_32 = [0u8; 32];
message_32[..8].copy_from_slice(&message);
The system enforces a 2/3 majority for consensus:
// If non_signers_count is more than 1/3 of registered operators, throw an error because quorum didn't meet
if non_signers_count > operator_count / 3 {
msg!(
"Quorum not met: non-signers count ({}) exceeds 1/3 of registered operators ({})",
non_signers_count,
operator_count
);
return Err(NCNProgramError::QuorumNotMet.into());
}
Only operators with sufficient stake can participate:
if signed {
let snapshot_epoch =
get_epoch(operator_snapshot.last_snapshot_slot(), ncn_epoch_length)?;
let current_epoch = get_epoch(current_slot, ncn_epoch_length)?;
let has_minimum_stake =
operator_snapshot.has_minimum_stake_now(current_epoch, snapshot_epoch)?;
if !has_minimum_stake {
msg!(
"The operator {} does not have enough stake to vote",
operator_snapshot.operator()
);
return Err(NCNProgramError::OperatorHasNoMinimumStake.into());
}
}
1. Admin creates Config with consensus parameters
2. Initialize VoteCounter for replay attack prevention
3. Initialize VaultRegistry for supported tokens
4. Initialize OperatorRegistry for participant tracking
5. Register supported stake token mints with weights
6. Register vaults (permissionless after NCN approval)
7. Register operators with BLS keypairs
8. Create Snapshot: mutable state checkpoint
9. Initialize operator snapshots for each participant
1. Create EpochState for new consensus round
2. Initialize WeightTable with current vault count
3. SetEpochWeights: Calculate voting power per vault
4. Snapshot vault-operator delegations
1. System reads current vote counter value as message
2. Operators generate BLS signatures on the counter message
3. Signatures are aggregated off-chain
4. CastVote instruction submits aggregated signature
5. Program verifies signature against current counter value
6. Vote counter is incremented after successful verification
7. Consensus reached at 66% threshold
The vote counter provides automatic replay attack protection:
- Unique Messages: Each vote uses a sequential counter value as the message
- Automatic Advancement: Counter increments after each successful vote
- Replay Prevention: Old signatures become invalid after counter advancement
- No External Dependencies: Message generation is deterministic and internal
1. Wait for epochs_after_consensus_before_close epochs
2. CloseEpochAccount reclaims rent from old accounts
3. Fee distribution to stakeholders
4. Prepare for next epoch cycle
-
Admin Commands: Configuration management
admin-create-config
: Initialize program parametersadmin-register-st-mint
: Add supported tokensadmin-set-parameters
: Update consensus settingsadmin-set-new-admin
: Change administrative rolesadmin-fund-account-payer
: Fund account payer
-
Crank Functions: State maintenance
crank-register-vaults
: Register pending vaultscrank-snapshot
: Snapshot operationscrank-snapshot-unupdated
: Snapshot unupdated operations
-
Instructions: Core program interactions
create-vault-registry
: Create vault registrycreate-vote-counter
: Initialize vote counterregister-vault
: Register vaultsregister-operator
: Register operators with BLS keyscreate-snapshot
: Create snapshotcreate-operator-snapshot
: Create operator snapshotsnapshot-vault-operator-delegation
: Capture delegationscast-vote
: Submit consensus votes with BLS aggregationgenerate-vote-signature
: Generate BLS signaturesaggregate-signatures
: Aggregate multiple BLS signatures
-
Getters: State queries
- Query any on-chain account state
- Inspect epoch progress and voting status
- Get operator stakes and vault information
The keeper automates epoch management through state transitions:
- Snapshot: Capture operator and vault states
- Vote: Monitor and process consensus votes
- Loop timeout: 10 minutes (configurable)
- Error timeout: 10 seconds
- Automatic state progression
- Error recovery and retry logic
Manages operator-specific functionality:
- BLS key generation and management
- Vote preparation and submission
- Delegation monitoring
- Reward claiming
simulation_test.rs
: Complete end-to-end consensus workflowfuzz_simulation_tests.rs
: Fuzz testing for consensus scenariosinitialize_config.rs
: Configuration testingregister_operator.rs
: Operator registration flowsupdate_operator_bn128_keys.rs
: BLS key update testingcast_vote.rs
: Voting mechanism and vote counter testingtest_cast_vote_counter_advancement
: Verifies counter increments correctlytest_cast_vote_duplicate_signature_fails
: Tests replay attack preventiontest_cast_vote_sequential_voting_with_counter_tracking
: Multi-round counter validationtest_cast_vote_wrong_counter_message_fails
: Invalid counter value rejection
snapshot_vault_operator_delegation.rs
: Delegation snapshot testinginitialize_operator_snapshot.rs
: Operator snapshot testinginitialize_vote_counter.rs
: Vote counter initialization testinginitialize_vault_registry.rs
: Vault registry testingregister_vault.rs
: Vault registration testingset_new_admin.rs
: Admin role management testingadmin_set_parameters.rs
: Parameter management testinginitialize_snapshot.rs
: Snapshot initialization testingrestaking_variations.rs
: Restaking integration testingmeta_tests.rs
: Meta-level testing utilities
The test_builder.rs
provides a comprehensive testing framework:
let mut fixture = TestBuilder::new().await;
fixture.initialize_restaking_and_vault_programs().await?;
fixture.create_test_ncn().await?;
fixture.add_vaults_to_test_ncn(&mut test_ncn, 1, Some(mint_keypair)).await?;
The main simulation test demonstrates:
- Setting up 13 test operators with BLS keypairs
- Creating vault-operator delegations
- Running complete epoch consensus cycle
- BLS signature aggregation and verification
- Consensus achievement and cleanup
- Pre-built program binaries in
integration_tests/tests/fixtures/
- Mock restaking and vault programs
- Pre-generated keypairs and test data
- Configurable test scenarios
The local-test-validator/
submodule provides a complete testing environment that automatically sets up the Jito Restaking Protocol with all dependencies. This is the fastest way to get a working development environment.
# Navigate to the local test validator directory
cd local-test-validator
# Make scripts executable
chmod +x *.sh
# Run the complete setup (one command does everything)
./run.sh
This single command will:
- Start Solana test validator with pre-loaded programs
- Initialize the complete restaking network
- Set up 3 operators with proper handshakes
- Create a vault and delegate tokens
- Advance validator by epochs for connection warm-up
The local test validator provides:
- Pre-built Programs: Jito restaking, vault, and SPL programs
- Automated Setup: Complete network initialization script
- Test Operators: 3 pre-configured operators with BLS keypairs
- Time Simulation: Scripts to advance validator time for epoch testing
- Clean State: Fresh ledger on each run with
--reset
run.sh
: Main orchestration script that sets up everythingvalidator.sh
: Starts Solana test validator with required programssetup-testing-env.sh
: Initializes the complete restaking networkrerun-validator.sh
: Advances validator time for epoch testing
After setup, you'll have:
- Keypairs: NCN admin, vault admin, operator admins in
./keys/
- Addresses: All important addresses saved in
setup_summary.txt
For detailed setup instructions and troubleshooting, see the local-test-validator README.
The project uses a Cargo workspace with the following members:
program/
- Main Solana programcore/
- Shared core functionalitycli/
- Command-line interfaceclients/rust/ncn_program/
- Auto-generated Rust clientintegration_tests/
- Comprehensive test suiteshank_cli/
- IDL generation tool
# Build all workspace components
cargo build --release
# Build Solana program
cargo build-sbf --manifest-path program/Cargo.toml
# Install CLI tools
cargo install --path ./cli --bin ncn-program-bls-cli --locked
- Solana: Custom Jito fork with BN254 support
- BLS Cryptography: BN254 curve operations via
solana-bn254
- Jito Integration: Restaking and vault program integration
- Code Generation: Kinobi + Exo Tech renderers for client generation
# 1. Build shank CLI for IDL generation
cargo b && ./target/debug/ncn-program-shank-cli
# 2. Install Node.js dependencies
yarn install
# 3. Generate Rust and JavaScript clients
yarn generate-clients
# 4. Rebuild with new clients
cargo b
The code generation pipeline uses:
- Kinobi: For IDL parsing and transformation
- Exo Tech Renderers: For generating Rust and JavaScript clients
- Custom Transformers: Convert PodU64/PodU128 to native types and add discriminators
Set up environment variables in .env
or shell:
export RPC_URL=http://127.0.0.1:8899
export COMMITMENT="confirmed"
export NCN_PROGRAM_ID="3fKQSi6VzzDUJSmeksS8qK6RB3Gs3UoZWtsQD3xagy45"
export RESTAKING_PROGRAM_ID="RestkWeAVL8fRGgzhfeoqFhsqKRchg6aa1XrcH96z4Q"
export VAULT_PROGRAM_ID="Vau1t6sLNxnzB7ZDsef8TLbPLfyZMYXH8WTNqUdm9g8"
export KEYPAIR_PATH="~/.config/solana/id.json"
# Create buffer account
solana-keygen new -o target/tmp/buffer.json
# Deploy program
solana program deploy --use-rpc --buffer target/tmp/buffer.json \
--with-compute-unit-price 10000 --max-sign-attempts 10000 \
target/deploy/ncn_program.so
# Upgrade existing program
solana program write-buffer --use-rpc --buffer target/tmp/buffer.json \
--with-compute-unit-price 10000 --max-sign-attempts 10000 \
target/deploy/ncn_program.so
solana program upgrade $(solana address --keypair target/tmp/buffer.json) \
$(solana address --keypair target/deploy/ncn_program-keypair.json)
# Clean up buffers
solana program close --buffers
- BLS Signature Verification: Cryptographic proof of operator consensus
- Vote Counter Replay Protection: Automatic prevention of signature replay attacks
- Minimum Stake Voting: Economic security through skin-in-the-game
- Time-locked Operations: Prevents hasty state changes
- Role-based Access Control: Admin separation and permissions
- Account Rent Protection: Economic incentives for proper cleanup
- Key Management: BLS private keys must be securely stored
- Counter Overflow: Theoretical risk after 2^64 votes (astronomically unlikely)
- Maximum Operators: 256 operators per NCN
- Maximum Vaults: Currently limited to 1 vault per registry
- Signature Verification: On-chain BLS verification costs
- Storage Costs: Large account sizes for snapshots
# Build the Solana program
cargo build-sbf
# Build the CLI tool
cargo build --bin ncn-program-bls-cli
Make sure you have your .env file with the proper environment variables
mv .env.example .env
Now inside .env, make sure to set the right NCN address which you can find in local-test-validator/keys/ncn/ncn_pubkey.text
, and the right VAULT
address which you can find in local-test-validator/keys/vault/vault_address.txt
# Deploy to local test validator or mainnet
solana program deploy --program-id ./ncn_program-keypair.json target/deploy/ncn_program.so
# Fund the payer account with 20 SOL for transaction fees
./target/debug/ncn-program-bls-cli admin-fund-account-payer --amount-in-sol 20
# Create and initialize the NCN program configuration
./target/debug/ncn-program-bls-cli admin-create-config \
--ncn-fee-wallet 3ogGQ7nFX6nCa9bkkZ6hwud6VaEQCekCCmNj6ZoWh8MF \
--ncn-fee-bps 100 \
--valid-slots-after-consensus 10000 \
--minimum-stake 100
# Initialize the vote counter for replay attack prevention
./target/debug/ncn-program-bls-cli create-vote-counter
# Create the vault registry to track supported stake vaults
./target/debug/ncn-program-bls-cli create-vault-registry
# Register a supported stake token mint and set its weight
./target/debug/ncn-program-bls-cli admin-register-st-mint
# Register vaults that are pending approval and add them to the registry
./target/debug/ncn-program-bls-cli crank-register-vaults
# Create the snapshot account
./target/debug/ncn-program-bls-cli create-snapshot
# Register operators with BLS keypairs (repeat for all operators)
./target/debug/ncn-program-bls-cli register-operator \
--operator <Operator Pubkey> \
--keypair-path <operator-admin-keypair>
# Update vault information first
./target/debug/ncn-program-bls-cli full-update-vault
# Snapshot the vault-operator delegations
./target/debug/ncn-program-bls-cli snapshot-vault-operator-delegation --operator <Operator Pubkey>
# Or you can snapshot all of them at once using
./target/debug/ncn-program-bls-cli crank-snapshot
# Generate signature using current vote counter as message
ncn-program-bls-cli generate-vote-signature \
--private-key <32_BYTE_HEX_PRIVATE_KEY>
# Generate signature for specific message
ncn-program-bls-cli generate-vote-signature \
--private-key <32_BYTE_HEX_PRIVATE_KEY> \
--message <32_BYTE_HEX_MESSAGE>
ncn-program-bls-cli aggregate-signatures \
--signatures <COMMA_SEPARATED_64_BYTE_HEX_SIGNATURES> \
--g1-public-keys <COMMA_SEPARATED_32_BYTE_HEX_G1_KEYS> \
--g2-public-keys <COMMA_SEPARATED_64_BYTE_HEX_G2_KEYS> \
--signers-bitmap <HEX_BITMAP>
Submit an aggregated vote to the NCN program.
ncn-program-bls-cli cast-vote \
--aggregated-signature <32_BYTE_HEX_AGGREGATED_SIGNATURE> \
--aggregated-g2 <64_BYTE_HEX_AGGREGATED_G2_KEY> \
--signers-bitmap <HEX_BITMAP> \
[--message <32_BYTE_HEX_MESSAGE>]