Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion bins/validator-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ parking_lot = { workspace = true }
sp-core = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
bincode = { workspace = true }
bincode = { workspace = true }
sha2 = { workspace = true }
55 changes: 55 additions & 0 deletions bins/validator-node/src/challenge_storage.rs
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())
Comment on lines +42 to +46

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 | 🟡 Minor

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 as challenge_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

‼️ 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
let mut hasher = Sha256::new();
hasher.update(challenge_id.as_bytes());
hasher.update(key);
hasher.update(value);
Ok(hasher.finalize().into())
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())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bins/validator-node/src/challenge_storage.rs` around lines 42 - 46, The
current hash construction in the block that creates a Sha256 hasher
(Sha256::new(), then hasher.update(challenge_id.as_bytes()), hasher.update(key),
hasher.update(value), hasher.finalize()) concatenates fields without delimiters
and can cause boundary collisions; change the hashing to include unambiguous
separators or length prefixes (e.g., write the length of challenge_id, key, and
value before each field or insert a fixed sentinel byte between fields) so the
input to hasher.update is canonical and collision-resistant; update the code
around the hasher usage (the hasher variable and the three hasher.update(...)
calls) to apply this length-prefix or delimiter strategy and then finalize as
before.

}

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()))
}
}
172 changes: 168 additions & 4 deletions bins/validator-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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

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 | 🟡 Minor

🧩 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 -C3

Repository: 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 -30

Repository: 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 -50

Repository: 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/null

Repository: 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 -40

Repository: 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/null

Repository: 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 -150

Repository: 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 -20

Repository: 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 -10

Repository: 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.rs

Repository: 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 -200

Repository: 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/null

Repository: 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 -20

Repository: 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 -30

Repository: 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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 -20

Repository: 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.rs

Repository: 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.rs

Repository: PlatformNetwork/platform-v2

Length of output: 2485


Extract job assignment timeout to a named constant.

The assignment.timestamp + 300_000 is a hardcoded 5-minute timeout that should be extracted to a named constant for clarity and maintainability.

Signature verification is already handled at the network layer in handle_gossipsub_message(), which validates all incoming messages before they reach the handlers, so no additional verification is needed here.

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
Verify each finding against the current code and only fix it if needed.

In `@bins/validator-node/src/main.rs` around lines 836 - 855, Replace the
hardcoded 300_000 timeout in the JobAssignment handler with a named constant
(e.g., JOB_ASSIGNMENT_TIMEOUT_MS) to improve clarity and maintainability: define
the constant near related types or at module scope and use it when computing
JobRecord.timeout_at (currently set as assignment.timestamp + 300_000) so it
becomes assignment.timestamp + JOB_ASSIGNMENT_TIMEOUT_MS; update any related
comments or docs and leave signature verification untouched since it's handled
in handle_gossipsub_message().

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

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

TaskProgress handler also processes state mutations without signature verification.

The TaskProgress handler creates a TaskProgressRecord and updates state via state.update_task_progress(record) without verifying the progress.signature. A malicious peer could forge progress updates for submissions it isn't assigned to evaluate.

Same guideline concern as the JobAssignment handler above.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bins/validator-node/src/main.rs` around lines 874 - 897, The TaskProgress
handler is mutating state without verifying the progress.signature; before
calling state.update_task_progress (inside the P2PMessage::TaskProgress arm
where TaskProgressRecord is built), validate the signature on progress using the
validator's public key and a canonical serialization of the progress data (e.g.,
submission_id, challenge_id, task_index, total_tasks, status, progress_pct,
timestamp) and only call state_manager.apply/update_task_progress if signature
verification succeeds; if verification fails, log and discard the message.
Ensure you perform the check on progress.signature and use the existing
signature verification utility in the codebase (or add one) so the verification
occurs prior to creating/updating the TaskProgressRecord and invoking
state.update_task_progress.

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) => {
Expand Down
11 changes: 8 additions & 3 deletions bins/validator-node/src/wasm_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info};
use wasm_runtime_interface::{
ExecPolicy, InMemoryStorageBackend, InstanceConfig, NetworkHostFunctions, NetworkPolicy,
NoopStorageBackend, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, StorageHostConfig,
StorageHostState, TimePolicy, WasmModule, WasmRuntime, WasmRuntimeError,
ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, NetworkHostFunctions,
NetworkPolicy, NoopStorageBackend, RuntimeConfig, SandboxHostFunctions, SandboxPolicy,
StorageHostConfig, StorageHostState, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime,
WasmRuntimeError,
};

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -393,6 +394,8 @@ impl WasmChallengeExecutor {
storage_host_config: StorageHostConfig::default(),
storage_backend: Arc::new(InMemoryStorageBackend::new()),
fixed_timestamp_ms: None,
consensus_policy: ConsensusPolicy::default(),
terminal_policy: TerminalPolicy::default(),
};

let mut instance = self
Expand Down Expand Up @@ -473,6 +476,8 @@ impl WasmChallengeExecutor {
storage_host_config: StorageHostConfig::default(),
storage_backend: Arc::new(InMemoryStorageBackend::new()),
fixed_timestamp_ms: None,
consensus_policy: ConsensusPolicy::default(),
terminal_policy: TerminalPolicy::default(),
};

let mut instance = self
Expand Down
60 changes: 60 additions & 0 deletions crates/challenge-sdk-wasm/src/host_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Disabled status (positive 1) slips through the status < 0 error check, causing data corruption.

The host-side ConsensusHostStatus::Disabled has value 1 (positive). When the host returns 1 for a disabled consensus, these wrappers treat it as a successful response of 1 byte, truncating the zero-initialized buffer to a single byte and returning Ok(vec![0]). The caller will misinterpret garbage as valid validator/vote data.

This applies to host_consensus_get_validators and host_consensus_get_votes. The same class of bug exists for host_consensus_get_state_hash (Line 262) where status == 1 is treated as success, though the impact there is returning a zeroed hash rather than corrupt data.

🐛 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 ConsensusHostStatus::Disabled to a negative value (e.g., -10) in consensus.rs so all non-success statuses are negative, aligning with the error-detection pattern used throughout the SDK. The same issue exists for TerminalHostStatus::Disabled = 1 in terminal.rs.

Also applies to: 252-260

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/challenge-sdk-wasm/src/host_functions.rs` around lines 234 - 242, The
wrappers host_consensus_get_validators, host_consensus_get_votes and
host_consensus_get_state_hash treat a returned status of 1
(ConsensusHostStatus::Disabled) as success and truncate the buffer; update each
function to treat status == 1 as an error (i.e., return Err(status)) rather than
truncating/returning data — only treat genuinely successful status values as
success (e.g., status > 1 for payload-returning calls), and keep the existing
negative-error check; change the conditional around the unsafe consensus_* call
in host_consensus_get_validators, host_consensus_get_votes and
host_consensus_get_state_hash to explicitly return Err(status) when status == 1
(or otherwise not in the valid-success range) so disabled responses are not
misinterpreted as data.


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() }
}
Loading