-
Notifications
You must be signed in to change notification settings - Fork 2
feat(infra): add P2P handlers, WASM host functions, and distributed storage for decentralized challenges #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
19f3690
d439cdd
df793ea
6910452
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| use platform_distributed_storage::{ | ||
| DistributedStore, GetOptions as DGetOptions, LocalStorage, PutOptions as DPutOptions, | ||
| StorageKey as DStorageKey, | ||
| }; | ||
| use sha2::{Digest, Sha256}; | ||
| use std::sync::Arc; | ||
| use wasm_runtime_interface::{StorageBackend, StorageHostError}; | ||
|
|
||
| pub struct ChallengeStorageBackend { | ||
| storage: Arc<LocalStorage>, | ||
| } | ||
|
|
||
| impl ChallengeStorageBackend { | ||
| pub fn new(storage: Arc<LocalStorage>) -> Self { | ||
| Self { storage } | ||
| } | ||
| } | ||
|
|
||
| impl StorageBackend for ChallengeStorageBackend { | ||
| fn get(&self, challenge_id: &str, key: &[u8]) -> Result<Option<Vec<u8>>, StorageHostError> { | ||
| let storage_key = DStorageKey::new(challenge_id, hex::encode(key)); | ||
| let result = tokio::runtime::Handle::current() | ||
| .block_on(self.storage.get(&storage_key, DGetOptions::default())) | ||
| .map_err(|e| StorageHostError::StorageError(e.to_string()))?; | ||
| Ok(result.map(|v| v.data)) | ||
| } | ||
|
|
||
| fn propose_write( | ||
| &self, | ||
| challenge_id: &str, | ||
| key: &[u8], | ||
| value: &[u8], | ||
| ) -> Result<[u8; 32], StorageHostError> { | ||
| let storage_key = DStorageKey::new(challenge_id, hex::encode(key)); | ||
| tokio::runtime::Handle::current() | ||
| .block_on( | ||
| self.storage | ||
| .put(storage_key, value.to_vec(), DPutOptions::default()), | ||
| ) | ||
| .map_err(|e| StorageHostError::StorageError(e.to_string()))?; | ||
|
|
||
| let mut hasher = Sha256::new(); | ||
| hasher.update(challenge_id.as_bytes()); | ||
| hasher.update(key); | ||
| hasher.update(value); | ||
| Ok(hasher.finalize().into()) | ||
| } | ||
|
|
||
| fn delete(&self, challenge_id: &str, key: &[u8]) -> Result<bool, StorageHostError> { | ||
| let storage_key = DStorageKey::new(challenge_id, hex::encode(key)); | ||
| tokio::runtime::Handle::current() | ||
| .block_on(self.storage.delete(&storage_key)) | ||
| .map_err(|e| StorageHostError::StorageError(e.to_string())) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| //! Uses libp2p for gossipsub consensus and Kademlia DHT for storage. | ||
| //! Submits weights to Bittensor at epoch boundaries. | ||
|
|
||
| mod challenge_storage; | ||
| mod wasm_executor; | ||
|
|
||
| use anyhow::Result; | ||
|
|
@@ -25,8 +26,9 @@ use platform_distributed_storage::{ | |
| DistributedStoreExt, LocalStorage, LocalStorageBuilder, StorageKey, | ||
| }; | ||
| use platform_p2p_consensus::{ | ||
| ChainState, ConsensusEngine, EvaluationMessage, EvaluationMetrics, EvaluationRecord, | ||
| NetworkEvent, P2PConfig, P2PMessage, P2PNetwork, StateManager, ValidatorRecord, ValidatorSet, | ||
| ChainState, ConsensusEngine, EvaluationMessage, EvaluationMetrics, EvaluationRecord, JobRecord, | ||
| JobStatus, NetworkEvent, P2PConfig, P2PMessage, P2PNetwork, StateManager, TaskProgressRecord, | ||
| ValidatorRecord, ValidatorSet, | ||
| }; | ||
| use std::path::{Path, PathBuf}; | ||
| use std::sync::Arc; | ||
|
|
@@ -419,6 +421,7 @@ async fn main() -> Result<()> { | |
| 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 wasm_eval_interval = tokio::time::interval(Duration::from_secs(5)); | ||
| let mut stale_job_interval = tokio::time::interval(Duration::from_secs(120)); | ||
|
|
||
| let (eval_broadcast_tx, mut eval_broadcast_rx) = tokio::sync::mpsc::channel::<P2PMessage>(256); | ||
|
|
||
|
|
@@ -518,6 +521,15 @@ async fn main() -> Result<()> { | |
| } | ||
| } | ||
|
|
||
| // Stale job cleanup | ||
| _ = stale_job_interval.tick() => { | ||
| let now = chrono::Utc::now().timestamp_millis(); | ||
| let stale = state_manager.apply(|state| state.cleanup_stale_jobs(now)); | ||
| if !stale.is_empty() { | ||
| info!(count = stale.len(), "Cleaned up stale jobs"); | ||
| } | ||
| } | ||
|
|
||
| // Periodic checkpoint | ||
| _ = checkpoint_interval.tick() => { | ||
| if let Some(handler) = shutdown_handler.as_mut() { | ||
|
|
@@ -784,8 +796,160 @@ async fn handle_network_event( | |
| } | ||
| }); | ||
| } | ||
| _ => { | ||
| debug!("Unhandled P2P message type"); | ||
| P2PMessage::StateRequest(req) => { | ||
| debug!( | ||
| requester = %req.requester.to_hex(), | ||
| sequence = req.current_sequence, | ||
| "Received state request" | ||
| ); | ||
| } | ||
| P2PMessage::StateResponse(resp) => { | ||
| debug!( | ||
| responder = %resp.responder.to_hex(), | ||
| sequence = resp.sequence, | ||
| "Received state response" | ||
| ); | ||
| } | ||
| P2PMessage::WeightVote(wv) => { | ||
| debug!( | ||
| validator = %wv.validator.to_hex(), | ||
| epoch = wv.epoch, | ||
| "Received weight vote" | ||
| ); | ||
| } | ||
| P2PMessage::PeerAnnounce(pa) => { | ||
| debug!( | ||
| validator = %pa.validator.to_hex(), | ||
| peer_id = %pa.peer_id, | ||
| addresses = pa.addresses.len(), | ||
| "Received peer announce" | ||
| ); | ||
| } | ||
| P2PMessage::JobClaim(claim) => { | ||
| info!( | ||
| validator = %claim.validator.to_hex(), | ||
| challenge_id = %claim.challenge_id, | ||
| max_jobs = claim.max_jobs, | ||
| "Received job claim" | ||
| ); | ||
| } | ||
| P2PMessage::JobAssignment(assignment) => { | ||
| info!( | ||
| submission_id = %assignment.submission_id, | ||
| challenge_id = %assignment.challenge_id, | ||
| assigned_validator = %assignment.assigned_validator.to_hex(), | ||
| assigner = %assignment.assigner.to_hex(), | ||
| "Received job assignment" | ||
| ); | ||
| let job = JobRecord { | ||
| submission_id: assignment.submission_id.clone(), | ||
| challenge_id: assignment.challenge_id, | ||
| assigned_validator: assignment.assigned_validator, | ||
| assigned_at: assignment.timestamp, | ||
| timeout_at: assignment.timestamp + 300_000, | ||
| status: JobStatus::Pending, | ||
| }; | ||
| state_manager.apply(|state| { | ||
| state.assign_job(job); | ||
| }); | ||
| } | ||
|
Comment on lines
+836
to
+855
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for signature verification patterns in the codebase
rg -n "verify.*signature|signature.*verify|verify_message|verify_signed" --type=rs -C3Repository: PlatformNetwork/platform-v2 Length of output: 97 🏁 Script executed: #!/bin/bash
# Search for timeout constants and magic numbers related to 300000
rg -n "300000|300_000|timeout.*const|TIMEOUT.*=|Job.*timeout" --type=rs | head -30Repository: PlatformNetwork/platform-v2 Length of output: 97 🏁 Script executed: #!/bin/bash
# Find where P2PMessage enum is defined and how it's processed
rg -n "enum P2PMessage|handle_network_event|process.*message" --type=rs -A5 | head -50Repository: PlatformNetwork/platform-v2 Length of output: 97 🏁 Script executed: #!/bin/bash
# Search for signature verification patterns - look in .rs files
find bins/validator-node/src -name "*.rs" -exec grep -n "verify.*signature\|signature.*verify\|verify_message\|verify_signed" {} + 2>/dev/nullRepository: PlatformNetwork/platform-v2 Length of output: 53 🏁 Script executed: #!/bin/bash
# Search for timeout constants and the 300_000 magic number
find bins/validator-node/src -name "*.rs" -exec grep -n "300_000\|TIMEOUT\|timeout.*const" {} + 2>/dev/null | head -40Repository: PlatformNetwork/platform-v2 Length of output: 170 🏁 Script executed: #!/bin/bash
# Find P2PMessage enum definition and handler structure
find . -name "*.rs" -exec grep -l "enum P2PMessage\|handle_network_event" {} + 2>/dev/nullRepository: PlatformNetwork/platform-v2 Length of output: 143 🏁 Script executed: #!/bin/bash
# Check the P2PMessage structure in messages.rs
cat crates/p2p-consensus/src/messages.rs | head -150Repository: PlatformNetwork/platform-v2 Length of output: 5110 🏁 Script executed: #!/bin/bash
# Search for where messages are received/parsed in main.rs to see if validation happens before the handler
grep -n "handle_network_event\|received.*message\|P2PMessage::" bins/validator-node/src/main.rs | head -20Repository: PlatformNetwork/platform-v2 Length of output: 1042 🏁 Script executed: #!/bin/bash
# Look for any network layer validation or message verification
find . -name "*.rs" -path "*/p2p*" -o -name "*.rs" -path "*/network*" 2>/dev/null | xargs grep -l "verify\|validate" 2>/dev/null | head -10Repository: PlatformNetwork/platform-v2 Length of output: 344 🏁 Script executed: #!/bin/bash
# Get the JobAssignmentMessage structure definition
grep -A20 "pub struct JobAssignmentMessage" crates/p2p-consensus/src/messages.rsRepository: PlatformNetwork/platform-v2 Length of output: 740 🏁 Script executed: #!/bin/bash
# Check the network.rs file for message validation/verification logic
cat crates/p2p-consensus/src/network.rs | head -200Repository: PlatformNetwork/platform-v2 Length of output: 6307 🏁 Script executed: #!/bin/bash
# Search for signature verification implementation
grep -rn "verify.*signature\|validate.*signature" crates/p2p-consensus/src/ 2>/dev/nullRepository: PlatformNetwork/platform-v2 Length of output: 877 🏁 Script executed: #!/bin/bash
# Search for SignedP2PMessage definition and usage
grep -rn "SignedP2PMessage\|struct Signed" crates/p2p-consensus/src/messages.rs | head -20Repository: PlatformNetwork/platform-v2 Length of output: 173 🏁 Script executed: #!/bin/bash
# Look for where messages are validated after reception but before handler
grep -n "from_bytes\|deserialize\|receive\|handle_message" crates/p2p-consensus/src/network.rs | head -30Repository: PlatformNetwork/platform-v2 Length of output: 574 🏁 Script executed: #!/bin/bash
# Check if there's validation logic around message processing
grep -B5 -A10 "P2PMessage::JobAssignment\|verify_signature.*assignment\|assignment.*verify" bins/validator-node/src/main.rsRepository: PlatformNetwork/platform-v2 Length of output: 824 🏁 Script executed: #!/bin/bash
# Get the full SignedP2PMessage struct definition
sed -n '610,650p' crates/p2p-consensus/src/messages.rsRepository: PlatformNetwork/platform-v2 Length of output: 1123 🏁 Script executed: #!/bin/bash
# Check how messages are actually dispatched in the event loop - look around line 432
sed -n '420,450p' bins/validator-node/src/main.rsRepository: PlatformNetwork/platform-v2 Length of output: 1355 🏁 Script executed: #!/bin/bash
# Search for any validation or signature checks on P2PMessage before handler dispatch
grep -B10 "handle_network_event" bins/validator-node/src/main.rs | grep -E "verify|validate|signature|sign"Repository: PlatformNetwork/platform-v2 Length of output: 53 🏁 Script executed: #!/bin/bash
# Confirm the pattern - check if Submission handler also doesn't verify signature
sed -n '723,760p' bins/validator-node/src/main.rsRepository: PlatformNetwork/platform-v2 Length of output: 1787 🏁 Script executed: #!/bin/bash
# Check where SignedP2PMessage gets unwrapped/deserialized
grep -n "SignedP2PMessage\|verify.*signature" crates/p2p-consensus/src/network.rs | head -20Repository: PlatformNetwork/platform-v2 Length of output: 405 🏁 Script executed: #!/bin/bash
# Get the full verify_message implementation
sed -n '420,460p' crates/p2p-consensus/src/network.rsRepository: PlatformNetwork/platform-v2 Length of output: 1549 🏁 Script executed: #!/bin/bash
# Search for where verify_message is actually called
grep -rn "verify_message" crates/p2p-consensus/src/ bins/validator-node/src/Repository: PlatformNetwork/platform-v2 Length of output: 263 🏁 Script executed: #!/bin/bash
# Look at the message deserialization flow - line 446 onwards
sed -n '440,500p' crates/p2p-consensus/src/network.rsRepository: PlatformNetwork/platform-v2 Length of output: 2485 Extract job assignment timeout to a named constant. The Signature verification is already handled at the network layer in Proposed constant extraction+/// Default job assignment timeout in milliseconds (5 minutes)
+const JOB_ASSIGNMENT_TIMEOUT_MS: i64 = 300_000;
+
// ... in the handler:
- timeout_at: assignment.timestamp + 300_000,
+ timeout_at: assignment.timestamp + JOB_ASSIGNMENT_TIMEOUT_MS,🤖 Prompt for AI Agents |
||
| P2PMessage::DataRequest(req) => { | ||
| debug!( | ||
| request_id = %req.request_id, | ||
| requester = %req.requester.to_hex(), | ||
| challenge_id = %req.challenge_id, | ||
| data_type = %req.data_type, | ||
| "Received data request" | ||
| ); | ||
| } | ||
| P2PMessage::DataResponse(resp) => { | ||
| debug!( | ||
| request_id = %resp.request_id, | ||
| responder = %resp.responder.to_hex(), | ||
| challenge_id = %resp.challenge_id, | ||
| data_bytes = resp.data.len(), | ||
| "Received data response" | ||
| ); | ||
| } | ||
| P2PMessage::TaskProgress(progress) => { | ||
| debug!( | ||
| submission_id = %progress.submission_id, | ||
| challenge_id = %progress.challenge_id, | ||
| validator = %progress.validator.to_hex(), | ||
| task_index = progress.task_index, | ||
| total_tasks = progress.total_tasks, | ||
| progress_pct = progress.progress_pct, | ||
| "Received task progress" | ||
| ); | ||
| let record = TaskProgressRecord { | ||
| submission_id: progress.submission_id.clone(), | ||
| challenge_id: progress.challenge_id, | ||
| validator: progress.validator, | ||
| task_index: progress.task_index, | ||
| total_tasks: progress.total_tasks, | ||
| status: progress.status, | ||
| progress_pct: progress.progress_pct, | ||
| updated_at: progress.timestamp, | ||
| }; | ||
| state_manager.apply(|state| { | ||
| state.update_task_progress(record); | ||
| }); | ||
| } | ||
|
Comment on lines
+874
to
+897
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TaskProgress handler also processes state mutations without signature verification. The Same guideline concern as the 🤖 Prompt for AI Agents |
||
| P2PMessage::TaskResult(result) => { | ||
| info!( | ||
| submission_id = %result.submission_id, | ||
| challenge_id = %result.challenge_id, | ||
| validator = %result.validator.to_hex(), | ||
| task_id = %result.task_id, | ||
| passed = result.passed, | ||
| score = result.score, | ||
| execution_time_ms = result.execution_time_ms, | ||
| "Received task result" | ||
| ); | ||
| } | ||
| P2PMessage::LeaderboardRequest(req) => { | ||
| debug!( | ||
| requester = %req.requester.to_hex(), | ||
| challenge_id = %req.challenge_id, | ||
| limit = req.limit, | ||
| offset = req.offset, | ||
| "Received leaderboard request" | ||
| ); | ||
| } | ||
| P2PMessage::LeaderboardResponse(resp) => { | ||
| debug!( | ||
| responder = %resp.responder.to_hex(), | ||
| challenge_id = %resp.challenge_id, | ||
| total_count = resp.total_count, | ||
| "Received leaderboard response" | ||
| ); | ||
| } | ||
| P2PMessage::ChallengeUpdate(update) => { | ||
| info!( | ||
| challenge_id = %update.challenge_id, | ||
| updater = %update.updater.to_hex(), | ||
| update_type = %update.update_type, | ||
| data_bytes = update.data.len(), | ||
| "Received challenge update" | ||
| ); | ||
| } | ||
| P2PMessage::StorageProposal(proposal) => { | ||
| debug!( | ||
| proposal_id = %hex::encode(&proposal.proposal_id[..8]), | ||
| challenge_id = %proposal.challenge_id, | ||
| proposer = %proposal.proposer.to_hex(), | ||
| key_len = proposal.key.len(), | ||
| value_len = proposal.value.len(), | ||
| "Received storage proposal" | ||
| ); | ||
| } | ||
| P2PMessage::StorageVote(vote) => { | ||
| debug!( | ||
| proposal_id = %hex::encode(&vote.proposal_id[..8]), | ||
| voter = %vote.voter.to_hex(), | ||
| approve = vote.approve, | ||
| "Received storage vote" | ||
| ); | ||
| } | ||
| }, | ||
| NetworkEvent::PeerConnected(peer_id) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -215,3 +215,63 @@ pub fn host_get_timestamp() -> i64 { | |
| pub fn host_log(level: u8, msg: &str) { | ||
| unsafe { log_message(level as i32, msg.as_ptr() as i32, msg.len() as i32) } | ||
| } | ||
|
|
||
| #[link(wasm_import_module = "platform_consensus")] | ||
| extern "C" { | ||
| fn consensus_get_epoch() -> i64; | ||
| fn consensus_get_validators(buf_ptr: i32, buf_len: i32) -> i32; | ||
| fn consensus_propose_weight(uid: i32, weight: i32) -> i32; | ||
| fn consensus_get_votes(buf_ptr: i32, buf_len: i32) -> i32; | ||
| fn consensus_get_state_hash(buf_ptr: i32) -> i32; | ||
| fn consensus_get_submission_count() -> i32; | ||
| fn consensus_get_block_height() -> i64; | ||
| } | ||
|
|
||
| pub fn host_consensus_get_epoch() -> i64 { | ||
| unsafe { consensus_get_epoch() } | ||
| } | ||
|
|
||
| pub fn host_consensus_get_validators() -> Result<Vec<u8>, i32> { | ||
| let mut buf = vec![0u8; 65536]; | ||
| let status = unsafe { consensus_get_validators(buf.as_mut_ptr() as i32, buf.len() as i32) }; | ||
| if status < 0 { | ||
| return Err(status); | ||
| } | ||
| buf.truncate(status as usize); | ||
| Ok(buf) | ||
| } | ||
|
Comment on lines
+234
to
+242
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The host-side This applies to 🐛 Proposed fix: check for positive non-success status codes pub fn host_consensus_get_validators() -> Result<Vec<u8>, i32> {
let mut buf = vec![0u8; 65536];
let status = unsafe { consensus_get_validators(buf.as_mut_ptr() as i32, buf.len() as i32) };
- if status < 0 {
+ if status < 0 || status == 1 {
+ // 1 = Disabled
return Err(status);
}
buf.truncate(status as usize);
Ok(buf)
} pub fn host_consensus_get_votes() -> Result<Vec<u8>, i32> {
let mut buf = vec![0u8; 65536];
let status = unsafe { consensus_get_votes(buf.as_mut_ptr() as i32, buf.len() as i32) };
- if status < 0 {
+ if status < 0 || status == 1 {
+ // 1 = Disabled
return Err(status);
}
buf.truncate(status as usize);
Ok(buf)
}Alternatively, consider a more robust fix: change Also applies to: 252-260 🤖 Prompt for AI Agents |
||
|
|
||
| pub fn host_consensus_propose_weight(uid: i32, weight: i32) -> Result<(), i32> { | ||
| let status = unsafe { consensus_propose_weight(uid, weight) }; | ||
| if status < 0 { | ||
| return Err(status); | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| pub fn host_consensus_get_votes() -> Result<Vec<u8>, i32> { | ||
| let mut buf = vec![0u8; 65536]; | ||
| let status = unsafe { consensus_get_votes(buf.as_mut_ptr() as i32, buf.len() as i32) }; | ||
| if status < 0 { | ||
| return Err(status); | ||
| } | ||
| buf.truncate(status as usize); | ||
| Ok(buf) | ||
| } | ||
|
|
||
| pub fn host_consensus_get_state_hash() -> Result<[u8; 32], i32> { | ||
| let mut buf = [0u8; 32]; | ||
| let status = unsafe { consensus_get_state_hash(buf.as_mut_ptr() as i32) }; | ||
| if status < 0 { | ||
| return Err(status); | ||
| } | ||
| Ok(buf) | ||
| } | ||
|
|
||
| pub fn host_consensus_get_submission_count() -> i32 { | ||
| unsafe { consensus_get_submission_count() } | ||
| } | ||
|
|
||
| pub fn host_consensus_get_block_height() -> i64 { | ||
| unsafe { consensus_get_block_height() } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hash over concatenated fields without delimiters can produce collisions.
SHA256(challenge_id || key || value)without length-prefixed or delimited fields is theoretically susceptible to boundary confusion (e.g.,challenge_id="ab"+key=[0x63]produces the same hash input aschallenge_id="abc"+key=[]). Practical risk is low if challenge IDs are UUIDs, but for correctness it's worth adding a separator or length prefix.Proposed fix
let mut hasher = Sha256::new(); hasher.update(challenge_id.as_bytes()); + hasher.update(b":"); hasher.update(key); + hasher.update(b":"); hasher.update(value); Ok(hasher.finalize().into())📝 Committable suggestion
🤖 Prompt for AI Agents