Skip to content

feat(storage): add validated storage with WASM-based consensus - #10

Merged
echobt merged 1 commit into
mainfrom
feat/wasm-validated-storage-consensus
Feb 16, 2026
Merged

feat(storage): add validated storage with WASM-based consensus#10
echobt merged 1 commit into
mainfrom
feat/wasm-validated-storage-consensus

Conversation

@echobt

@echobt echobt commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a per-challenge validated storage system where validators must reach consensus before data is accepted. WASM code defines validation rules, and writes only succeed when a configurable quorum of validators agree.

Changes

  • Add validated_storage module to distributed-storage crate with:
    • ValidatedStorage struct for per-challenge storage with consensus
    • StorageWriteProposal and StorageWriteVote types for the proposal/vote flow
    • WasmStorageValidator trait and DefaultWasmValidator implementation
    • Configurable quorum size and proposal expiration
    • Cryptographic signatures on all proposals and votes
  • Add storage module to wasm-runtime-interface crate with host functions for WASM-based validation
  • Add sha2 dependency for content hashing
  • Re-export validated storage types from the distributed-storage crate root

Breaking Changes

None - this is a new additive feature.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added validated storage mechanism with consensus-driven writes, proposal tracking, and quorum-based vote verification
    • Introduced storage host interface with configuration, audit logging, and per-challenge namespacing
    • Added support for pluggable storage backends with validation and error handling

Add per-challenge storage system where validators must reach consensus
before data is accepted. This prevents abuse by requiring WASM-defined
validation logic and cryptographic signatures on all proposals and votes.

Core implementation in distributed-storage crate:
- ValidatedStorage: main struct managing proposals, votes, and commits
- StorageWriteProposal: represents a write request with value hash
- StorageWriteVote: validator vote with optional WASM validation result
- ConsensusResult: tracks consensus state and commitment status
- WasmStorageValidator trait: interface for WASM validation logic
- DefaultWasmValidator: passthrough implementation for testing
- ValidatedStorageConfig: configurable quorum size and timeouts

WASM runtime interface integration:
- ChallengeStorage: high-level API for WASM modules to access storage
- ChallengeStorageConfig: per-challenge storage configuration
- Full async support with proposal/vote/commit lifecycle

Key features:
- Configurable quorum size for consensus requirements
- Proposal expiration with automatic cleanup
- Duplicate and conflicting vote detection
- Cryptographic hashing of values (SHA256)
- Comprehensive test coverage for all operations

Dependencies:
- Added sha2 to wasm-runtime-interface for hashing
@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces a comprehensive WASM-based validated storage system for consensus-driven writes across two crates. The distributed-storage crate gains per-challenge proposal and voting mechanisms with quorum-based consensus tracking. The wasm-runtime-interface crate adds a storage host interface with configurable backends, audit logging, and result encoding for storage operations.

Changes

Cohort / File(s) Summary
Validated Storage Core
crates/distributed-storage/src/validated_storage.rs
Introduces ValidatedStorage wrapping a DistributedStore with proposal creation, per-challenge voting, consensus evaluation, and write commitment. Defines StorageWriteProposal, StorageWriteVote, ConsensusResult, and WasmValidationResult types. Includes WasmStorageValidator trait and DefaultWasmValidator implementation. Comprehensive error handling via ValidatedStorageError. Provides lifecycle APIs: propose_write, receive_proposal, vote_on_proposal, receive_vote, check_consensus, commit_write, get, and cleanup_expired. Includes internal state tracking and test coverage.
Distributed Storage Module Exports
crates/distributed-storage/src/lib.rs
Declares and re-exports the new validated_storage module, exposing ValidatedStorage, ValidatedStorageConfig, StorageWriteProposal, StorageWriteVote, WasmValidationResult, ConsensusResult, ValidatedStorageError, WasmStorageValidator, and DefaultWasmValidator as public API.
Storage Host Interface
crates/wasm-runtime-interface/src/storage.rs
Introduces comprehensive storage host abstraction with StorageHostConfig for validation and quotas, StorageBackend trait with Get/ProposeWrite/Delete operations, and two implementations: NoopStorageBackend (consensus-only mode) and InMemoryStorageBackend (per-challenge namespaced storage). Defines request/response types (StorageGetRequest, StorageProposeWriteRequest, StorageDeleteRequest), state machine (StorageHostState), packed result encoding (pack_result/unpack_result), audit structures (StorageAuditEntry, StorageOperation, StorageAuditLogger), and comprehensive error/status handling. Includes unit tests covering validation, backend behavior, and state management.
WASM Runtime Module Exports
crates/wasm-runtime-interface/src/lib.rs
Declares and re-exports the new storage module, exposing StorageBackend, storage implementations (InMemoryStorageBackend, NoopStorageBackend), request/response types, state and config types, host operation constants, audit types, and error handling via StorageHostError and StorageHostStatus.
Dependencies
crates/wasm-runtime-interface/Cargo.toml
Adds sha2 workspace dependency for proposal identification and hashing functionality.

Sequence Diagram

sequenceDiagram
    participant Proposer as Proposer
    participant VS as ValidatedStorage
    participant DB as DistributedStore
    participant Voters as Voter Pool

    Proposer->>VS: propose_write(key, value)
    VS->>VS: create StorageWriteProposal
    VS->>VS: store proposal state
    VS-->>Proposer: proposal created

    Proposer->>Voters: broadcast proposal
    Voters->>VS: receive_proposal(proposal)
    VS->>VS: validate proposal
    VS-->>Voters: proposal acknowledged

    Note over Voters: Voting phase
    Voters->>VS: vote_on_proposal(proposal_id, approved, validator)
    VS->>VS: verify voter signature
    VS->>VS: detect duplicates/conflicts
    VS->>VS: track vote counts
    VS-->>Voters: vote recorded

    Proposer->>VS: check_consensus(proposal_id)
    VS->>VS: count approvals vs quorum
    alt Consensus Reached
        VS->>VS: create ConsensusResult
        VS->>DB: commit_write(key, value)
        DB-->>VS: write confirmed
        VS-->>Proposer: committed
    else Consensus Failed
        VS-->>Proposer: consensus pending/failed
    end

    Proposer->>VS: get(key)
    VS->>DB: retrieve if committed
    DB-->>VS: return value
    VS-->>Proposer: value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A burrow of storage, now locked tight,
With votes and proposals, consensus in flight,
Each write passes quorum before it takes hold,
A WASM-bound guardian, steady and bold! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: introducing a validated storage system with WASM-based consensus. It is concise, specific, and clearly summarizes the primary objective of the changeset.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/wasm-validated-storage-consensus

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@crates/distributed-storage/src/validated_storage.rs`:
- Around line 327-333: ValidatedStorage currently never invokes
WasmStorageValidator so WASM validation is unused; add a validator field to
ValidatedStorage (e.g., validator: Option<Arc<dyn WasmStorageValidator + Send +
Sync>>) and populate it when building ValidatedStorage based on the
require_wasm_validation flag, then call validator.validate_write(...) inside
vote_on_proposal and receive_proposal (use the same inputs those methods already
have: proposal payload and metadata) before creating/recording a vote or
accepting a proposal; on validation failure return/reject the vote path (or log
and mark proposal as invalid in proposals/committed) so invalid writes do not
proceed.
- Around line 685-699: cleanup_expired currently keeps proposals with
consensus_result.is_some() forever and there is no cleanup for the committed
map, causing memory growth; update cleanup_expired (and/or add a new
cleanup_committed) to evict consensus-reached proposals after a TTL or when
their consensus timestamp is older than config.proposal_timeout_ms (or a new
config like committed_ttl_ms), and add symmetric eviction for the committed map
(the committed field) using the same TTL or a max-size policy so entries in
committed are removed once stale; locate and update the proposals.retain closure
in cleanup_expired and implement a new cleanup path that checks
state.consensus_result.timestamp (or stores a timestamp when setting
consensus_result) and the committed map pruning logic to remove stale entries.
- Around line 148-158: The structs StorageWriteProposal and StorageWriteVote
declare signature: Vec<u8> but signatures are never produced or checked, so
implement end-to-end signing and verification: when creating proposals/votes
(where StorageWriteProposal and StorageWriteVote instances are constructed) sign
the canonical serialized payload with the proposer's/voter's Hotkey private key
and populate signature, and when validating incoming proposals/votes verify the
signature against the Hotkey public key and return the InvalidSignature error
variant on failure; if you intend to defer implementation instead add an
explicit TODO/issue reference in the creation/validation code and comment on
StorageWriteProposal/StorageWriteVote to avoid misleading claims about
cryptographic signatures.
- Around line 127-135: The constructor new currently allows quorum_size == 0
which makes check_consensus treat approving.len() >= quorum_size as true
immediately; update new (the function new that sets the quorum_size field) to
enforce a minimum bound (e.g., require quorum_size >= 1) by either
asserting/validating and panicking on invalid input or normalizing the value
(e.g., quorum_size = std::cmp::max(1, quorum_size)); ensure the change
references quorum_size and that check_consensus/approving.len() behavior is no
longer able to auto-consensus with zero votes.

In `@crates/wasm-runtime-interface/src/storage.rs`:
- Around line 335-354: The propose_write implementation currently inserts values
immediately into the main store (the HashMap under self.data) which bypasses
consensus; change propose_write (InMemoryStorageBackend::propose_write) to only
record the proposal (e.g., store the key/value in a dedicated proposals map or a
proposals entry keyed by the returned proposal id) and return the SHA-256
proposal id, deferring any mutation of the primary data store until a separate
commit method (e.g., commit_write/commit_proposal) applies the proposal;
alternatively, if immediate persistence is intentionally allowed for tests, add
a clear doc comment on InMemoryStorageBackend::propose_write stating it bypasses
consensus semantics and/or gate it behind a test-only flag.
🧹 Nitpick comments (5)
crates/wasm-runtime-interface/src/lib.rs (1)

16-21: Consider merging the two pub use storage:: blocks and checking for missing re-exports.

There are two separate pub use storage::{...} blocks (lines 16–21 and 33–36). These can be merged into one for tidiness. Also, NoopStorageAuditLogger, pack_result, and unpack_result are public in storage.rs but not re-exported here. If they're intended as internal-only, consider reducing their visibility in storage.rs; otherwise, re-export them for consumers.

Also applies to: 33-36

crates/distributed-storage/src/validated_storage.rs (2)

65-65: Blanket #![allow(...)] suppresses useful warnings — scope it down.

#![allow(dead_code, unused_variables, unused_imports)] at the module level masks real issues (e.g., the unused WasmStorageValidator integration, unused require_wasm_validation field). Consider removing this and addressing the warnings individually, or scoping the allows to specific items that are intentionally stubbed.


519-581: check_consensus acquires a write lock even when only reading.

Line 523 takes self.proposals.write().await unconditionally. When consensus is already decided (lines 528–530), only a read is needed. Under contention (many validators polling), this serializes all callers unnecessarily.

Consider a two-phase approach: read lock first, then upgrade to write only when consensus is newly reached.

crates/wasm-runtime-interface/src/storage.rs (2)

23-23: Same blanket #![allow(...)] concern as in validated_storage.rs.

Consider removing or scoping this down to specific items.


270-281: StorageBackend implementations don't enforce StorageHostConfig limits.

StorageHostConfig defines validate_key and validate_value, but neither InMemoryStorageBackend nor NoopStorageBackend calls these methods. A caller that bypasses the host state layer can store arbitrarily large keys/values. Consider either validating inside the backend methods or documenting that callers must validate externally.

Also applies to: 322-367

Comment on lines +127 to +135
pub fn new(challenge_id: &str, quorum_size: usize) -> Self {
Self {
challenge_id: challenge_id.to_string(),
quorum_size,
proposal_timeout_ms: 30_000,
namespace_prefix: format!("validated:{}", challenge_id),
require_wasm_validation: true,
}
}

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

quorum_size = 0 allows auto-consensus with zero votes.

No validation on quorum_size in the constructor. Setting it to 0 makes approving.len() >= 0 always true (line 546), so check_consensus would reach consensus before any votes are cast. Add a minimum bound.

Proposed fix
     pub fn new(challenge_id: &str, quorum_size: usize) -> Self {
+        assert!(quorum_size >= 1, "quorum_size must be at least 1");
         Self {
             challenge_id: challenge_id.to_string(),
             quorum_size,
📝 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
pub fn new(challenge_id: &str, quorum_size: usize) -> Self {
Self {
challenge_id: challenge_id.to_string(),
quorum_size,
proposal_timeout_ms: 30_000,
namespace_prefix: format!("validated:{}", challenge_id),
require_wasm_validation: true,
}
}
pub fn new(challenge_id: &str, quorum_size: usize) -> Self {
assert!(quorum_size >= 1, "quorum_size must be at least 1");
Self {
challenge_id: challenge_id.to_string(),
quorum_size,
proposal_timeout_ms: 30_000,
namespace_prefix: format!("validated:{}", challenge_id),
require_wasm_validation: true,
}
}
🤖 Prompt for AI Agents
In `@crates/distributed-storage/src/validated_storage.rs` around lines 127 - 135,
The constructor new currently allows quorum_size == 0 which makes
check_consensus treat approving.len() >= quorum_size as true immediately; update
new (the function new that sets the quorum_size field) to enforce a minimum
bound (e.g., require quorum_size >= 1) by either asserting/validating and
panicking on invalid input or normalizing the value (e.g., quorum_size =
std::cmp::max(1, quorum_size)); ensure the change references quorum_size and
that check_consensus/approving.len() behavior is no longer able to
auto-consensus with zero votes.

Comment on lines +148 to +158
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StorageWriteProposal {
pub proposal_id: [u8; 32],
pub challenge_id: String,
pub proposer: Hotkey,
pub key: Vec<u8>,
pub value: Vec<u8>,
pub value_hash: [u8; 32],
pub timestamp: i64,
pub signature: Vec<u8>,
}

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

Signature fields on proposals and votes are never populated or verified.

Both StorageWriteProposal (line 157) and StorageWriteVote (line 227) have signature: Vec<u8> fields that are always initialized to empty vectors. The InvalidSignature error variant exists but is never returned. The PR description claims "cryptographic signatures on proposals and votes," but this is not implemented.

If signatures are planned for a follow-up, consider documenting that explicitly (e.g., a TODO or tracking issue). Otherwise, unsigned proposals/votes undermine the security model since any node can forge votes from arbitrary hotkeys.

Would you like me to open an issue to track the signature implementation?

Also applies to: 220-228

🤖 Prompt for AI Agents
In `@crates/distributed-storage/src/validated_storage.rs` around lines 148 - 158,
The structs StorageWriteProposal and StorageWriteVote declare signature: Vec<u8>
but signatures are never produced or checked, so implement end-to-end signing
and verification: when creating proposals/votes (where StorageWriteProposal and
StorageWriteVote instances are constructed) sign the canonical serialized
payload with the proposer's/voter's Hotkey private key and populate signature,
and when validating incoming proposals/votes verify the signature against the
Hotkey public key and return the InvalidSignature error variant on failure; if
you intend to defer implementation instead add an explicit TODO/issue reference
in the creation/validation code and comment on
StorageWriteProposal/StorageWriteVote to avoid misleading claims about
cryptographic signatures.

Comment on lines +327 to +333
pub struct ValidatedStorage<S: DistributedStore> {
inner: Arc<S>,
config: ValidatedStorageConfig,
local_hotkey: Hotkey,
proposals: Arc<RwLock<HashMap<[u8; 32], ProposalState>>>,
committed: Arc<RwLock<HashMap<[u8; 32], ConsensusResult>>>,
}

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

WasmStorageValidator is defined but never integrated into ValidatedStorage.

The WasmStorageValidator trait (line 702) and the require_wasm_validation config field (line 123) exist, but ValidatedStorage has no field for a validator and never calls validate_write during vote_on_proposal or receive_proposal. This means WASM validation—the core feature described in the PR title—is effectively a no-op.

Consider adding a validator: Arc<dyn WasmStorageValidator> field to ValidatedStorage and calling it during the vote phase:

Proposed structural change
 pub struct ValidatedStorage<S: DistributedStore> {
     inner: Arc<S>,
     config: ValidatedStorageConfig,
     local_hotkey: Hotkey,
     proposals: Arc<RwLock<HashMap<[u8; 32], ProposalState>>>,
     committed: Arc<RwLock<HashMap<[u8; 32], ConsensusResult>>>,
+    validator: Arc<dyn WasmStorageValidator>,
 }

Also applies to: 702-722

🤖 Prompt for AI Agents
In `@crates/distributed-storage/src/validated_storage.rs` around lines 327 - 333,
ValidatedStorage currently never invokes WasmStorageValidator so WASM validation
is unused; add a validator field to ValidatedStorage (e.g., validator:
Option<Arc<dyn WasmStorageValidator + Send + Sync>>) and populate it when
building ValidatedStorage based on the require_wasm_validation flag, then call
validator.validate_write(...) inside vote_on_proposal and receive_proposal (use
the same inputs those methods already have: proposal payload and metadata)
before creating/recording a vote or accepting a proposal; on validation failure
return/reject the vote path (or log and mark proposal as invalid in
proposals/committed) so invalid writes do not proceed.

Comment on lines +685 to +699
pub async fn cleanup_expired(&self) -> usize {
let mut proposals = self.proposals.write().await;
let timeout = self.config.proposal_timeout_ms;
let before = proposals.len();

proposals.retain(|_, state| {
!state.proposal.is_expired(timeout) || state.consensus_result.is_some()
});

let removed = before - proposals.len();
if removed > 0 {
debug!(removed, "Cleaned up expired proposals");
}
removed
}

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

committed map and consensus-reached proposals are never cleaned up — potential memory leak.

cleanup_expired (line 690–692) retains proposals where consensus_result.is_some(), and the committed map (line 332) has no cleanup at all. In a long-running node with many challenges, both maps grow without bound. Consider adding a TTL or max-size eviction for committed results.

🤖 Prompt for AI Agents
In `@crates/distributed-storage/src/validated_storage.rs` around lines 685 - 699,
cleanup_expired currently keeps proposals with consensus_result.is_some()
forever and there is no cleanup for the committed map, causing memory growth;
update cleanup_expired (and/or add a new cleanup_committed) to evict
consensus-reached proposals after a TTL or when their consensus timestamp is
older than config.proposal_timeout_ms (or a new config like committed_ttl_ms),
and add symmetric eviction for the committed map (the committed field) using the
same TTL or a max-size policy so entries in committed are removed once stale;
locate and update the proposals.retain closure in cleanup_expired and implement
a new cleanup path that checks state.consensus_result.timestamp (or stores a
timestamp when setting consensus_result) and the committed map pruning logic to
remove stale entries.

Comment on lines +335 to +354
fn propose_write(
&self,
challenge_id: &str,
key: &[u8],
value: &[u8],
) -> Result<[u8; 32], StorageHostError> {
let mut data = self
.data
.write()
.map_err(|e| StorageHostError::InternalError(format!("lock poisoned: {}", e)))?;
let challenge_data: &mut HashMap<Vec<u8>, Vec<u8>> =
data.entry(challenge_id.to_string()).or_default();
challenge_data.insert(key.to_vec(), value.to_vec());

let mut hasher = Sha256::new();
hasher.update(challenge_id.as_bytes());
hasher.update(key);
hasher.update(value);
Ok(hasher.finalize().into())
}

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

InMemoryStorageBackend::propose_write immediately persists data, bypassing consensus semantics.

The method name propose_write (from the StorageBackend trait) implies a proposal that awaits consensus, but this implementation inserts the value directly into the store (line 347). This is semantically inconsistent with the validated-storage design where writes should only succeed after quorum approval.

If this is intentional for testing/development, consider documenting it clearly (e.g., a doc comment on the impl). If not, the backend should only record the proposed key/value and return a proposal ID, deferring actual insertion to a separate commit step.

🤖 Prompt for AI Agents
In `@crates/wasm-runtime-interface/src/storage.rs` around lines 335 - 354, The
propose_write implementation currently inserts values immediately into the main
store (the HashMap under self.data) which bypasses consensus; change
propose_write (InMemoryStorageBackend::propose_write) to only record the
proposal (e.g., store the key/value in a dedicated proposals map or a proposals
entry keyed by the returned proposal id) and return the SHA-256 proposal id,
deferring any mutation of the primary data store until a separate commit method
(e.g., commit_write/commit_proposal) applies the proposal; alternatively, if
immediate persistence is intentionally allowed for tests, add a clear doc
comment on InMemoryStorageBackend::propose_write stating it bypasses consensus
semantics and/or gate it behind a test-only flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant