@@ -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.
7790async 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| {
0 commit comments