Skip to content
870 changes: 856 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ members = [
"crates/distributed-storage",
"crates/challenge-sdk",
"crates/challenge-registry",
"crates/challenge-loader",
"crates/epoch",
"crates/bittensor-integration",
"crates/subnet-manager",
"crates/rpc-server",
"crates/challenge-orchestrator",
"crates/secure-container-runtime",
"crates/p2p-consensus",
"crates/wasm-runtime",
"bins/validator-node",
"bins/utils",
"tests",
Expand Down
2 changes: 2 additions & 0 deletions bins/validator-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ platform-distributed-storage = { path = "../../crates/distributed-storage" }
platform-challenge-sdk = { path = "../../crates/challenge-sdk" }
challenge-orchestrator = { path = "../../crates/challenge-orchestrator" }
secure-container-runtime = { path = "../../crates/secure-container-runtime" }
platform-wasm-runtime = { path = "../../crates/wasm-runtime" }
platform-challenge-loader = { path = "../../crates/challenge-loader" }

# Bittensor
bittensor-rs = { workspace = true }
Expand Down
131 changes: 129 additions & 2 deletions bins/validator-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ use clap::Parser;
use parking_lot::RwLock;
use platform_bittensor::{
sync_metagraph, BittensorClient, BlockSync, BlockSyncConfig, BlockSyncEvent, Metagraph,
Subtensor, SubtensorClient,
StorageConfig, StorageReader, Subtensor, SubtensorClient,
};
use platform_challenge_loader::{ChallengeLoader, LoaderConfig};
use platform_core::{
checkpoint::{
CheckpointData, CheckpointManager, CompletedEvaluationState, PendingEvaluationState,
Expand All @@ -23,6 +24,8 @@ use platform_distributed_storage::{
DistributedStoreExt, LocalStorage, LocalStorageBuilder, StorageKey,
};
use platform_p2p_consensus::{
assignment::{AssignmentConfig, ValidatorAssignment},
fast_consensus::{FastConsensus, FastConsensusConfig},
ChainState, ConsensusEngine, NetworkEvent, P2PConfig, P2PMessage, P2PNetwork, StateManager,
ValidatorRecord, ValidatorSet,
};
Expand Down Expand Up @@ -159,6 +162,14 @@ struct Args {
/// Docker challenges support
#[arg(long, default_value = "true")]
docker_challenges: bool,

/// Challenge modules directory
#[arg(long, default_value = "./challenges")]
challenges_dir: PathBuf,

/// Enable WASM challenge loading
#[arg(long, default_value = "true")]
wasm_challenges: bool,
}

// ==================== Main ====================
Expand All @@ -183,6 +194,33 @@ async fn main() -> Result<()> {
let validator_hotkey = keypair.ss58_address();
info!("Validator hotkey: {}", validator_hotkey);

// Initialize challenge loader
let challenge_loader = if args.wasm_challenges {
let loader_config = LoaderConfig {
challenges_dir: Some(args.challenges_dir.clone()),
enable_p2p_discovery: true,
max_challenges: 100,
..Default::default()
};

match ChallengeLoader::new(loader_config) {
Ok(loader) => {
info!("Challenge loader initialized");
Some(Arc::new(loader))
}
Err(e) => {
warn!(
"Failed to initialize challenge loader: {}. WASM challenges disabled.",
e
);
None
}
}
} else {
info!("WASM challenges disabled");
None
};

Comment on lines +197 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "start_discovery" --type rs

Repository: PlatformNetwork/platform-v2

Length of output: 97


🏁 Script executed:

# Find the ChallengeLoader struct definition and its methods
fd "challenge" --type f -e rs | head -20

Repository: PlatformNetwork/platform-v2

Length of output: 289


🏁 Script executed:

# Look for ChallengeLoader implementation
ast-grep --pattern 'impl $_ {
  $$$
  fn start_discovery($_) {
    $$$
  }
  $$$
}'

Repository: PlatformNetwork/platform-v2

Length of output: 53


🏁 Script executed:

# Find challenge-loader crate
fd "challenge-loader" -type d

Repository: PlatformNetwork/platform-v2

Length of output: 241


🏁 Script executed:

# Search for start_discovery without file type restriction
rg "start_discovery"

Repository: PlatformNetwork/platform-v2

Length of output: 346


🏁 Script executed:

# Find ChallengeLoader struct definition
rg "struct ChallengeLoader" -A 10

Repository: PlatformNetwork/platform-v2

Length of output: 857


🏁 Script executed:

# Read the loader.rs file to understand start_discovery implementation
cat crates/challenge-loader/src/loader.rs | head -200

Repository: PlatformNetwork/platform-v2

Length of output: 5922


🏁 Script executed:

# Look for the new() method implementation
rg -A 30 "pub fn new\(" crates/challenge-loader/src/loader.rs

Repository: PlatformNetwork/platform-v2

Length of output: 1066


🏁 Script executed:

# Check what start_discovery does
rg -A 20 "pub async fn start_discovery" crates/challenge-loader/src/loader.rs

Repository: PlatformNetwork/platform-v2

Length of output: 926


🏁 Script executed:

# Get the complete start_discovery implementation
rg -A 50 "pub async fn start_discovery" crates/challenge-loader/src/loader.rs

Repository: PlatformNetwork/platform-v2

Length of output: 2006


🏁 Script executed:

# Check if there are any other places where start_discovery is called on ChallengeLoader instances
rg "start_discovery\(\)" -B 2 -A 2

Repository: PlatformNetwork/platform-v2

Length of output: 743


🏁 Script executed:

# Check the context of main.rs around lines 197-223
cat -n crates/validator-node/src/main.rs | sed -n '180,240p'

Repository: PlatformNetwork/platform-v2

Length of output: 136


🏁 Script executed:

# Look for the function that contains this code
rg -B 20 "Initialize challenge loader" crates/validator-node/src/main.rs | head -40

Repository: PlatformNetwork/platform-v2

Length of output: 144


🏁 Script executed:

# Find the validator-node main.rs file
fd "main.rs" | grep -i validator

Repository: PlatformNetwork/platform-v2

Length of output: 102


🏁 Script executed:

# Look for validator-node directory
fd "validator-node" -type d

Repository: PlatformNetwork/platform-v2

Length of output: 241


🏁 Script executed:

# Read the validator-node main.rs file around the specified lines
cat -n bins/validator-node/src/main.rs | sed -n '180,240p'

Repository: PlatformNetwork/platform-v2

Length of output: 2349


🏁 Script executed:

# Check the complete context of the function containing the challenge loader initialization
rg -B 30 "Initialize challenge loader" bins/validator-node/src/main.rs | head -60

Repository: PlatformNetwork/platform-v2

Length of output: 945


🏁 Script executed:

# Look for how the challenge_loader is used after initialization
rg -A 50 "Initialize challenge loader" bins/validator-node/src/main.rs | head -80

Repository: PlatformNetwork/platform-v2

Length of output: 1680


🏁 Script executed:

# Check if challenge_loader is used anywhere else in the code
rg "challenge_loader" bins/validator-node/src/main.rs -A 3 -B 1

Repository: PlatformNetwork/platform-v2

Length of output: 818


🏁 Script executed:

# Search for any calls to start_discovery on challenge_loader in the entire validator-node
rg "challenge_loader.*start_discovery" bins/validator-node/

Repository: PlatformNetwork/platform-v2

Length of output: 53


🏁 Script executed:

# Check what methods are called on challenge_loader
rg "challenge_loader\." bins/validator-node/src/ -A 1

Repository: PlatformNetwork/platform-v2

Length of output: 263


🏁 Script executed:

# Check the complete context of the discovery interval loop to see full usage
rg -B 10 -A 10 "challenge_discovery_interval" bins/validator-node/src/main.rs

Repository: PlatformNetwork/platform-v2

Length of output: 2017


🏁 Script executed:

# See if there's any initialization after the challenge_loader assignment
cat -n bins/validator-node/src/main.rs | sed -n '197,350p' | grep -A 150 "Initialize challenge loader"

Repository: PlatformNetwork/platform-v2

Length of output: 6466


🏁 Script executed:

# Check if list_challenges requires discovery to be running
rg -A 20 "pub fn list_challenges" crates/challenge-loader/src/loader.rs

Repository: PlatformNetwork/platform-v2

Length of output: 768


🏁 Script executed:

# Check if there are any other methods that would populate challenges
rg "pub.*fn.*challenge" crates/challenge-loader/src/loader.rs | head -20

Repository: PlatformNetwork/platform-v2

Length of output: 489


🏁 Script executed:

# Verify that the registry is empty on initialization
rg -A 10 "pub fn with_capacity" crates/challenge-loader/src/ | grep -A 10 registry

Repository: PlatformNetwork/platform-v2

Length of output: 831


🏁 Script executed:

# Check if there's any initialization code that might auto-load challenges
rg "fn new\|impl.*ChallengeRegistry" crates/challenge-loader/src/ -A 15 | head -50

Repository: PlatformNetwork/platform-v2

Length of output: 53


Call start_discovery() on the ChallengeLoader after initialization.

The loader is created with enable_p2p_discovery: true and a challenges_dir, but start_discovery() is never invoked. Without this call, the discovery sources are never registered, no initial discovery occurs, and list_challenges() will perpetually return an empty list. The polling in the main loop will never find any challenges.

Suggested fix
        match ChallengeLoader::new(loader_config) {
            Ok(loader) => {
+                if let Err(e) = loader.start_discovery().await {
+                    warn!("Failed to start challenge discovery: {}", e);
+                }
                 info!("Challenge loader initialized");
                 Some(Arc::new(loader))
            }
🤖 Prompt for AI Agents
In `@bins/validator-node/src/main.rs` around lines 197 - 223, The ChallengeLoader
is initialized with enable_p2p_discovery but never started; after constructing
the loader in the Ok(loader) arm (the local variable named loader used to create
Some(Arc::new(loader))), call loader.start_discovery() and handle its Result
(log an error/warn and return None or proceed on Ok) so discovery sources are
registered and list_challenges() will populate; update the Ok branch around
ChallengeLoader::new(...) to invoke start_discovery() before wrapping the loader
in Arc and adjust logging on failure.

// Create data directory
std::fs::create_dir_all(&args.data_dir)?;
let data_dir = std::fs::canonicalize(&args.data_dir)?;
Expand Down Expand Up @@ -210,6 +248,19 @@ async fn main() -> Result<()> {
let validator_set = Arc::new(ValidatorSet::new(keypair.clone(), p2p_config.min_stake));
info!("P2P network config initialized");

// Initialize validator assignment
let assignment_config = AssignmentConfig {
min_validators: 3,
max_validators: 10,
stake_weighted: true,
epoch_seed: [0u8; 32], // Will be updated from Bittensor
};
let validator_assignment = Arc::new(RwLock::new(ValidatorAssignment::new(
validator_set.clone(),
assignment_config,
)));
info!("Validator assignment initialized");

// Initialize state manager, loading persisted state if available
let state_manager = Arc::new(
load_state_from_storage(&storage, args.netuid)
Expand Down Expand Up @@ -242,6 +293,19 @@ async fn main() -> Result<()> {
state_manager.clone(),
)));

// Initialize fast validation consensus
let fast_consensus_config = FastConsensusConfig {
finality_threshold: 0.67,
vote_timeout: Duration::from_secs(5),
max_score_variance: 0.1,
};
let _fast_consensus = Arc::new(RwLock::new(FastConsensus::new(
keypair.clone(),
validator_set.clone(),
fast_consensus_config,
)));
info!("Fast validation consensus initialized");
Comment on lines +296 to +307

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

FastConsensus is created but not wired to Evaluation messages.

_fast_consensus is never used and evaluation messages are only logged, so no vote aggregation occurs.

Also applies to: 735-742

🤖 Prompt for AI Agents
In `@bins/validator-node/src/main.rs` around lines 296 - 307, FastConsensus is
instantiated as _fast_consensus but never used, so Evaluation messages are only
logged and no vote aggregation happens; locate where Evaluation messages are
handled (the processing loop or handler that currently logs evaluations) and
wire in the FastConsensus instance by storing Arc<RwLock<FastConsensus>> (the
_fast_consensus you created) into the surrounding state/context, then replace
the simple log path with calls into FastConsensus methods (e.g.,
FastConsensus::record_evaluation or FastConsensus::handle_evaluation — whatever
method on FastConsensus aggregates votes) so incoming Evaluation messages are
forwarded to _fast_consensus for vote aggregation and consensus progression;
ensure you clone the Arc when moving into async handlers and acquire the RwLock
appropriately before calling the aggregation methods.


// Connect to Bittensor
let subtensor: Option<Arc<Subtensor>>;
let subtensor_signer: Option<Arc<BittensorSigner>>;
Expand Down Expand Up @@ -338,6 +402,29 @@ async fn main() -> Result<()> {
bittensor_client_for_metagraph = None;
}

// Initialize storage reader for direct metagraph access
let _storage_reader = if !args.no_bittensor {
let storage_config = StorageConfig {
endpoint: args.subtensor_endpoint.clone(),
netuid: args.netuid,
cache_duration_secs: 60,
max_retries: 3,
};
let mut reader = StorageReader::new(storage_config);
match reader.connect().await {
Ok(()) => {
info!("Bittensor storage reader connected");
Some(Arc::new(RwLock::new(reader)))
}
Err(e) => {
warn!("Storage reader connection failed: {}", e);
None
}
}
} else {
None
};

// Initialize shutdown handler for graceful checkpoint persistence
let mut shutdown_handler =
match ShutdownHandler::new(&data_dir, state_manager.clone(), args.netuid) {
Expand All @@ -363,6 +450,7 @@ async fn main() -> Result<()> {
let mut stale_check_interval = tokio::time::interval(Duration::from_secs(60));
let mut state_persist_interval = tokio::time::interval(Duration::from_secs(60));
let mut checkpoint_interval = tokio::time::interval(Duration::from_secs(300)); // 5 minutes
let mut challenge_discovery_interval = tokio::time::interval(Duration::from_secs(120));

loop {
tokio::select! {
Expand All @@ -389,6 +477,7 @@ async fn main() -> Result<()> {
&subtensor_signer,
&subtensor_client,
&state_manager,
&validator_assignment,
netuid,
version_key,
).await;
Expand Down Expand Up @@ -449,6 +538,21 @@ async fn main() -> Result<()> {
}
}

// Challenge discovery check
_ = challenge_discovery_interval.tick() => {
if let Some(loader) = challenge_loader.as_ref() {
let challenges = loader.list_challenges();
if !challenges.is_empty() {
debug!("Active challenges: {}", challenges.len());
for challenge in challenges.iter().take(5) {
debug!(" - {} (v{})", challenge.name, challenge.version);
}
} else {
debug!("No challenges loaded");
}
}
}

// Ctrl+C
_ = tokio::signal::ctrl_c() => {
info!("Received shutdown signal, creating final checkpoint...");
Expand Down Expand Up @@ -628,8 +732,16 @@ async fn handle_network_event(
debug!("Heartbeat update skipped: {}", e);
}
}
P2PMessage::Evaluation(eval_msg) => {
// Handle evaluation messages which may contain validation results
// These are processed through the FastConsensus module for vote aggregation
debug!(
"Received evaluation message for challenge {:?} from {:?}",
eval_msg.challenge_id, source
);
}
_ => {
debug!("Unhandled P2P message type");
debug!("Unhandled P2P message type from {:?}", source);
}
},
NetworkEvent::PeerConnected(peer_id) => {
Expand Down Expand Up @@ -661,6 +773,7 @@ async fn handle_block_event(
signer: &Option<Arc<BittensorSigner>>,
_client: &Option<SubtensorClient>,
state_manager: &Arc<StateManager>,
validator_assignment: &Arc<RwLock<ValidatorAssignment>>,
netuid: u16,
version_key: u64,
) {
Expand All @@ -682,6 +795,20 @@ async fn handle_block_event(
old_epoch, new_epoch, block
);

// Update validator assignment epoch seed
{
let mut seed = [0u8; 32];
seed[..8].copy_from_slice(&new_epoch.to_le_bytes());
seed[8..16].copy_from_slice(&block.to_le_bytes());
validator_assignment
.write()
.update_config(AssignmentConfig {
epoch_seed: seed,
..Default::default()
});
Comment on lines +798 to +808

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find AssignmentConfig definition and related code
rg -n "struct AssignmentConfig|impl.*Default.*AssignmentConfig|fn update_config" --type rs -A 10 -B 2

Repository: PlatformNetwork/platform-v2

Length of output: 97


🏁 Script executed:

#!/bin/bash
# Find AssignmentConfig definition and related code
rg -n "struct AssignmentConfig|impl.*Default.*AssignmentConfig|fn update_config" crates/ -A 10 -B 2

Repository: PlatformNetwork/platform-v2

Length of output: 3475


🏁 Script executed:

#!/bin/bash
# Check the actual code context in the validator-node main.rs file
sed -n '790,820p' bins/validator-node/src/main.rs

Repository: PlatformNetwork/platform-v2

Length of output: 1162


Preserve existing validator assignment config during epoch transitions.

Calling update_config(AssignmentConfig { epoch_seed: seed, ..Default::default() }) replaces the entire configuration, resetting min_validators to 3, max_validators to 10, and stake_weighted to true on every epoch transition. This overwrites any previously tuned values. Clone the existing config and update only the epoch_seed field:

Suggested fix
-                validator_assignment
-                    .write()
-                    .update_config(AssignmentConfig {
-                        epoch_seed: seed,
-                        ..Default::default()
-                    });
+                let mut cfg = validator_assignment.read().config().clone();
+                cfg.epoch_seed = seed;
+                validator_assignment.write().update_config(cfg);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update validator assignment epoch seed
{
let mut seed = [0u8; 32];
seed[..8].copy_from_slice(&new_epoch.to_le_bytes());
seed[8..16].copy_from_slice(&block.to_le_bytes());
validator_assignment
.write()
.update_config(AssignmentConfig {
epoch_seed: seed,
..Default::default()
});
// Update validator assignment epoch seed
{
let mut seed = [0u8; 32];
seed[..8].copy_from_slice(&new_epoch.to_le_bytes());
seed[8..16].copy_from_slice(&block.to_le_bytes());
let mut cfg = validator_assignment.read().config().clone();
cfg.epoch_seed = seed;
validator_assignment.write().update_config(cfg);
🤖 Prompt for AI Agents
In `@bins/validator-node/src/main.rs` around lines 798 - 808, The current call to
validator_assignment.write().update_config(AssignmentConfig { epoch_seed: seed,
..Default::default() }) replaces the whole AssignmentConfig and resets tuned
fields; instead, read or clone the existing AssignmentConfig from
validator_assignment, modify only its epoch_seed with the new seed, and pass
that updated config into update_config so min_validators, max_validators,
stake_weighted, etc. are preserved (use the existing validator_assignment
read/clone, update epoch_seed, then write().update_config(updated_config)).

debug!("Updated assignment epoch seed for epoch {}", new_epoch);
}

// Transition state to next epoch
state_manager.apply(|state| {
state.next_epoch();
Expand Down
5 changes: 5 additions & 0 deletions crates/bittensor-integration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod block_sync;
mod challenge_weight_collector;
mod client;
mod config;
pub mod storage;
mod validator_sync;
mod weights;

Expand All @@ -31,6 +32,10 @@ pub use block_sync::*;
pub use challenge_weight_collector::*;
pub use client::*;
pub use config::*;
pub use storage::{
MetagraphSnapshot, StakeInfo, StorageConfig, StorageError, StorageReader, ValidatorInfo,
WeightEntry,
};
pub use validator_sync::*;
pub use weights::*;

Expand Down
Loading