Skip to content

Commit c211432

Browse files
committed
fix: audit fixes - signature verification, dedup broadcast, state hash completeness, sequence tracking
1 parent 372a86c commit c211432

File tree

4 files changed

+117
-35
lines changed

4 files changed

+117
-35
lines changed

bins/validator-node/src/challenge_storage.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,9 @@ impl StorageBackend for ChallengeStorageBackend {
297297
}
298298
}
299299

300-
let result: Vec<(Vec<u8>, Vec<u8>)> = items.into_iter().collect();
300+
// Sort by key for deterministic ordering across calls
301+
let mut result: Vec<(Vec<u8>, Vec<u8>)> = items.into_iter().collect();
302+
result.sort_by(|a, b| a.0.cmp(&b.0));
301303
let limited = if result.len() > limit as usize {
302304
result[..limit as usize].to_vec()
303305
} else {

bins/validator-node/src/main.rs

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ fn sanitize_for_log(s: &str) -> String {
7272
.collect()
7373
}
7474

75+
/// Verify an sr25519 signature from a Hotkey over the given data.
76+
fn verify_signature(signer: &Hotkey, data: &[u8], signature: &[u8]) -> bool {
77+
use sp_core::crypto::Pair as _;
78+
if signature.len() != 64 {
79+
return false;
80+
}
81+
let mut sig_bytes = [0u8; 64];
82+
sig_bytes.copy_from_slice(signature);
83+
let sig = sp_core::sr25519::Signature::from_raw(sig_bytes);
84+
let pubkey = sp_core::sr25519::Public::from_raw(signer.0);
85+
sp_core::sr25519::Pair::verify(&sig, data, &pubkey)
86+
}
87+
7588
/// Helper to mutate ChainState and automatically persist changes.
7689
/// This ensures we never forget to persist after mutations.
7790
async fn mutate_and_persist<F, R>(
@@ -1789,36 +1802,46 @@ async fn main() -> Result<()> {
17891802
if let Some(ref executor) = wasm_executor {
17901803
match executor.execute_sync_with_block(&module_path, current_block, current_epoch) {
17911804
Ok(sync_result) => {
1792-
info!(
1793-
challenge_id = %challenge_id,
1794-
block = current_block,
1795-
total_users = sync_result.total_users,
1796-
"Challenge sync completed, broadcasting proposal"
1797-
);
1805+
// Skip broadcasting if sync was deduped (zeroed hash
1806+
// from DedupFlags guard). Broadcasting a zeroed hash
1807+
// would poison P2P consensus.
1808+
if sync_result.leaderboard_hash == [0u8; 32] && sync_result.total_users == 0 {
1809+
debug!(
1810+
challenge_id = %challenge_id,
1811+
"Sync deduped (zeroed result), skipping broadcast"
1812+
);
1813+
} else {
1814+
info!(
1815+
challenge_id = %challenge_id,
1816+
block = current_block,
1817+
total_users = sync_result.total_users,
1818+
"Challenge sync completed, broadcasting proposal"
1819+
);
17981820

1799-
// Broadcast sync proposal
1800-
let timestamp = chrono::Utc::now().timestamp_millis();
1801-
let proposal_data = bincode::serialize(&(
1802-
&challenge_id,
1803-
&sync_result.leaderboard_hash,
1804-
current_block,
1805-
timestamp
1806-
)).unwrap_or_default();
1807-
let signature = keypair.sign_bytes(&proposal_data).unwrap_or_default();
1808-
1809-
let proposal_msg = P2PMessage::ChallengeSyncProposal(
1810-
platform_p2p_consensus::ChallengeSyncProposalMessage {
1811-
challenge_id,
1812-
sync_result_hash: sync_result.leaderboard_hash,
1813-
proposer: keypair.hotkey(),
1814-
block_number: current_block,
1815-
timestamp,
1816-
signature,
1817-
}
1818-
);
1821+
// Broadcast sync proposal
1822+
let timestamp = chrono::Utc::now().timestamp_millis();
1823+
let proposal_data = bincode::serialize(&(
1824+
&challenge_id,
1825+
&sync_result.leaderboard_hash,
1826+
current_block,
1827+
timestamp
1828+
)).unwrap_or_default();
1829+
let signature = keypair.sign_bytes(&proposal_data).unwrap_or_default();
1830+
1831+
let proposal_msg = P2PMessage::ChallengeSyncProposal(
1832+
platform_p2p_consensus::ChallengeSyncProposalMessage {
1833+
challenge_id,
1834+
sync_result_hash: sync_result.leaderboard_hash,
1835+
proposer: keypair.hotkey(),
1836+
block_number: current_block,
1837+
timestamp,
1838+
signature,
1839+
}
1840+
);
18191841

1820-
if let Err(e) = p2p_broadcast_tx.send(platform_p2p_consensus::P2PCommand::Broadcast(proposal_msg)).await {
1821-
warn!(error = %e, "Failed to broadcast sync proposal");
1842+
if let Err(e) = p2p_broadcast_tx.send(platform_p2p_consensus::P2PCommand::Broadcast(proposal_msg)).await {
1843+
warn!(error = %e, "Failed to broadcast sync proposal");
1844+
}
18221845
}
18231846
}
18241847
Err(e) => {
@@ -3015,11 +3038,30 @@ async fn handle_network_event(
30153038
// Verify proposer is a known validator
30163039
let proposer_valid = validator_set.is_validator(&proposal.proposer);
30173040

3041+
// Verify cryptographic signature on proposal
3042+
let sig_valid = if proposer_valid {
3043+
let sign_data = bincode::serialize(&(
3044+
&proposal.proposal_id,
3045+
proposal.challenge_id.to_string(),
3046+
proposal.timestamp,
3047+
))
3048+
.unwrap_or_default();
3049+
verify_signature(&proposal.proposer, &sign_data, &proposal.signature)
3050+
} else {
3051+
false
3052+
};
3053+
30183054
if !proposer_valid {
30193055
warn!(
30203056
proposer = %proposal.proposer.to_ss58(),
30213057
"Storage proposal from unknown validator, ignoring"
30223058
);
3059+
} else if !sig_valid {
3060+
warn!(
3061+
proposal_id = %hex::encode(&proposal.proposal_id[..8]),
3062+
proposer = %proposal.proposer.to_ss58(),
3063+
"Storage proposal signature verification failed, ignoring"
3064+
);
30233065
} else {
30243066
// Add proposal to state
30253067
let storage_proposal = StorageProposal {
@@ -3151,8 +3193,25 @@ async fn handle_network_event(
31513193
);
31523194

31533195
// Verify voter is a known validator
3154-
if !validator_set.is_validator(&vote.voter) {
3196+
// Verify cryptographic signature on vote
3197+
let voter_known = validator_set.is_validator(&vote.voter);
3198+
let vote_sig_valid = if voter_known {
3199+
let vote_sign_data =
3200+
bincode::serialize(&(&vote.proposal_id, vote.approve, vote.timestamp))
3201+
.unwrap_or_default();
3202+
verify_signature(&vote.voter, &vote_sign_data, &vote.signature)
3203+
} else {
3204+
false
3205+
};
3206+
3207+
if !voter_known {
31553208
warn!(voter = %vote.voter.to_ss58(), "Vote from unknown validator");
3209+
} else if !vote_sig_valid {
3210+
warn!(
3211+
proposal_id = %hex::encode(&vote.proposal_id[..8]),
3212+
voter = %vote.voter.to_ss58(),
3213+
"Storage vote signature verification failed, ignoring"
3214+
);
31563215
} else {
31573216
// Add vote to proposal
31583217
let consensus_result = state_manager.apply(|state| {

bins/validator-node/src/wasm_executor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,7 @@ impl WasmChallengeExecutor {
11951195
..self.config.storage_host_config.clone()
11961196
},
11971197
storage_backend: Arc::clone(&self.config.storage_backend),
1198-
consensus_policy: ConsensusPolicy::read_only(),
1198+
consensus_policy: ConsensusPolicy::default(),
11991199
network_policy: NetworkPolicy::development(),
12001200
time_policy: TimePolicy::deterministic(real_now_ms),
12011201
block_height,

crates/p2p-consensus/src/state.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ impl ChainState {
478478
pending_ids: Vec<String>,
479479
weight_voter_count: usize,
480480
historical_weight_epochs: Vec<u64>,
481+
completed_eval_count: usize,
482+
leaderboard_challenges: Vec<String>,
483+
active_job_ids: Vec<String>,
484+
task_progress_ids: Vec<String>,
481485
}
482486

483487
let mut validators: Vec<(String, u64)> = self
@@ -503,6 +507,19 @@ impl ChainState {
503507
self.historical_weights.keys().copied().collect();
504508
historical_weight_epochs.sort();
505509

510+
let completed_eval_count: usize =
511+
self.completed_evaluations.values().map(|v| v.len()).sum();
512+
513+
let mut leaderboard_challenges: Vec<String> =
514+
self.leaderboard.keys().map(|c| c.to_string()).collect();
515+
leaderboard_challenges.sort();
516+
517+
let mut active_job_ids: Vec<String> = self.active_jobs.keys().cloned().collect();
518+
active_job_ids.sort();
519+
520+
let mut task_progress_ids: Vec<String> = self.task_progress.keys().cloned().collect();
521+
task_progress_ids.sort();
522+
506523
let input = HashInput {
507524
sequence: self.sequence,
508525
epoch: self.epoch,
@@ -514,6 +531,10 @@ impl ChainState {
514531
pending_ids,
515532
weight_voter_count,
516533
historical_weight_epochs,
534+
completed_eval_count,
535+
leaderboard_challenges,
536+
active_job_ids,
537+
task_progress_ids,
517538
};
518539

519540
self.state_hash = hash_data(&input).unwrap_or([0u8; 32]);
@@ -911,7 +932,7 @@ impl ChainState {
911932

912933
if let Some(record) = self.pending_evaluations.get_mut(submission_id) {
913934
record.evaluations.insert(validator, evaluation);
914-
self.update_hash();
935+
self.increment_sequence();
915936
Ok(())
916937
} else {
917938
Err(StateError::ChallengeNotFound(submission_id.to_string()))
@@ -1360,7 +1381,7 @@ impl ChainState {
13601381
pub fn update_task_progress(&mut self, record: TaskProgressRecord) {
13611382
let key = format!("{}:{}", record.submission_id, record.validator.to_hex());
13621383
self.task_progress.insert(key, record);
1363-
self.update_hash();
1384+
self.increment_sequence();
13641385
}
13651386

13661387
pub fn update_challenge_storage_root(&mut self, challenge_id: ChallengeId, root: [u8; 32]) {
@@ -1473,7 +1494,7 @@ impl ChainState {
14731494
timestamp: chrono::Utc::now().timestamp_millis(),
14741495
},
14751496
);
1476-
self.update_hash();
1497+
self.increment_sequence();
14771498
return true;
14781499
}
14791500
}
@@ -1522,7 +1543,7 @@ impl ChainState {
15221543
.entry(submission_id.to_string())
15231544
.or_default()
15241545
.insert(validator, logs_data);
1525-
self.update_hash();
1546+
self.increment_sequence();
15261547
true
15271548
}
15281549

0 commit comments

Comments
 (0)