This document provides detailed sequence diagrams for all major extrinsics in the Scalable Web3 Storage system, explaining the flow of data between clients, providers, and the blockchain.
- Overview
- Why Checkpoints Require Provider Signatures
- Provider Registration
- Bucket Creation
- Storage Agreements
- Data Upload Flow
- Checkpoint (Commitment) Flow
- Data Read Flow
- Challenge Flow
- Layer 1: Drive Operations
The system has a clear separation between:
- On-chain operations: Executed as blockchain extrinsics (transactions)
- Off-chain operations: HTTP requests to provider nodes
┌─────────────────────────────────────────────────────────────────────────┐
│ Trust Boundaries │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Client │◄───────►│ Provider │ │ Blockchain │ │
│ │ │ HTTP │ Node │ │ (Pallet) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ ▲ │
│ │ │ │ │
│ └────────────────────────┴────────────────────────┘ │
│ Extrinsics (signed transactions) │
│ │
│ Trust Level: │
│ • Blockchain: Trustless (consensus-verified) │
│ • Provider HTTP: Accountable (signature + stake + challenge) │
│ • Client: Application-specific │
│ │
└─────────────────────────────────────────────────────────────────────────┘
When a client uploads data to a provider, how do we ensure the provider actually stores it? The provider could:
- Accept the data, discard it, and claim storage payment
- Store it initially but delete it later
- Serve data only when convenient
Provider signatures on checkpoints create non-repudiable evidence:
┌─────────────────────────────────────────────────────────────────────────┐
│ CommitmentPayload (what providers sign) │
├─────────────────────────────────────────────────────────────────────────┤
│ { │
│ version: 1, // Protocol version │
│ bucket_id: u64, // Which bucket │
│ mmr_root: H256, // Merkle Mountain Range root │
│ start_seq: u64, // First leaf index │
│ leaf_count: u64, // Number of leaves │
│ } │
├─────────────────────────────────────────────────────────────────────────┤
│ By signing this, the provider attests: │
│ "I have stored all data corresponding to this MMR root" │
│ │
│ The signature becomes EVIDENCE for: │
│ 1. On-chain challenges (challenge_checkpoint) │
│ 2. Off-chain challenges (challenge_offchain) │
│ 3. Slashing if provider cannot produce data │
└─────────────────────────────────────────────────────────────────────────┘
The client could submit a checkpoint claiming the provider stored data, but:
- The provider might not have the data
- There's no evidence linking the provider to the commitment
- Challenges would be unfair (provider didn't agree to store)
Provider signature = Provider's agreement to be held accountable
For buckets with multiple providers, we need consensus:
┌─────────────────────────────────────────────────────────────────────────┐
│ Checkpoint Threshold Requirement │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Example: Bucket with 3 primary providers │
│ │
│ Provider A signs: ✓ │
│ Provider B signs: ✓ │
│ Provider C signs: ✗ (unavailable) │
│ │
│ Threshold: 51% must sign │
│ Result: 2/3 = 66.7% ✓ Checkpoint accepted │
│ │
│ Bitfield stored on-chain: 0b00000011 │
│ (bit 0 = Provider A, bit 1 = Provider B) │
│ │
│ Only signed providers can be challenged for this checkpoint! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
sequenceDiagram
participant P as Provider
participant C as Chain (Pallet)
participant B as Balances
P->>C: register_provider(multiaddr, public_key, capacity, stake)
Note over C: Validate inputs
C->>C: ensure!(stake >= MinProviderStake)
C->>C: ensure!(public_key is valid format)
C->>B: Currency::reserve(provider, stake)
Note over B: Lock stake tokens
C->>C: Create ProviderInfo {
Note over C: multiaddr,
Note over C: public_key,
Note over C: stake,
Note over C: committed_bytes: 0,
Note over C: settings: Default,
Note over C: stats: Empty
Note over C: }
C->>C: Providers::insert(provider, info)
C-->>P: Event::ProviderRegistered { provider, stake, capacity }
sequenceDiagram
participant P as Provider
participant C as Chain (Pallet)
P->>C: update_provider_settings(settings)
Note over C: settings = {
Note over C: min_duration: 100,
Note over C: max_duration: 100000,
Note over C: price_per_byte: 1000000,
Note over C: accepting_primary: true,
Note over C: replica_sync_price: Some(10M),
Note over C: accepting_extensions: true
Note over C: }
C->>C: info = Providers::get(provider)?
C->>C: info.settings = new_settings
C->>C: Providers::insert(provider, info)
C-->>P: Event::ProviderSettingsUpdated { provider }
sequenceDiagram
participant U as User (Admin)
participant C as Chain (Pallet)
U->>C: create_bucket(is_private, min_providers)
Note over C: Generate new bucket_id
C->>C: bucket_id = NextBucketId::get()
C->>C: NextBucketId::put(bucket_id + 1)
Note over C: Create bucket structure
C->>C: bucket = Bucket {
Note over C: admin: caller,
Note over C: is_private,
Note over C: min_providers,
Note over C: primary_providers: vec![],
Note over C: snapshot: None,
Note over C: members: BTreeMap::new()
Note over C: }
C->>C: Buckets::insert(bucket_id, bucket)
C->>C: AdminBuckets::append(admin, bucket_id)
C-->>U: Event::BucketCreated { bucket_id, admin }
sequenceDiagram
participant A as Admin
participant C as Chain (Pallet)
participant B as Balances
A->>C: request_agreement(bucket_id, provider, max_bytes, duration, max_payment)
Note over C: Validate bucket and provider
C->>C: bucket = Buckets::get(bucket_id)?
C->>C: ensure!(bucket.admin == caller)
C->>C: provider_info = Providers::get(provider)?
C->>C: ensure!(provider_info.settings.accepting_primary)
Note over C: Calculate actual payment
C->>C: payment = price_per_byte × max_bytes × duration
C->>C: ensure!(payment <= max_payment)
Note over C: Reserve payment
C->>B: Currency::reserve(admin, payment)
Note over C: Create pending request
C->>C: AgreementRequests::insert((bucket_id, provider), request)
C-->>A: Event::AgreementRequested { bucket_id, provider, max_bytes }
sequenceDiagram
participant P as Provider
participant C as Chain (Pallet)
P->>C: accept_agreement(bucket_id)
Note over C: Get pending request
C->>C: request = AgreementRequests::take((bucket_id, caller))?
Note over C: Create agreement
C->>C: agreement = StorageAgreement {
Note over C: provider: caller,
Note over C: bucket_id,
Note over C: max_bytes: request.max_bytes,
Note over C: start_block: current_block,
Note over C: end_block: current_block + duration,
Note over C: payment: request.payment,
Note over C: role: ProviderRole::Primary
Note over C: }
C->>C: StorageAgreements::insert((bucket_id, provider), agreement)
Note over C: Add to bucket's provider list
C->>C: bucket.primary_providers.push(provider)
C->>C: Buckets::insert(bucket_id, bucket)
Note over C: Update provider stats
C->>C: provider_info.committed_bytes += max_bytes
C-->>P: Event::AgreementAccepted { bucket_id, provider }
This is the primary off-chain flow where data is actually stored:
sequenceDiagram
participant U as User
participant SC as Storage Client
participant PN as Provider Node
participant S as Storage Layer
Note over U,S: Step 1: Upload Chunks
U->>SC: upload(bucket_id, data)
SC->>SC: Split data into 256 KiB chunks
SC->>SC: Build Merkle tree of chunks
SC->>SC: data_root = merkle_root(chunks)
loop For each chunk
SC->>PN: PUT /node { bucket_id, hash, data }
PN->>S: store_node(bucket_id, hash, data)
PN-->>SC: { stored: true }
end
Note over U,S: Step 2: Commit to MMR
SC->>PN: POST /commit { bucket_id, data_roots: [data_root] }
PN->>S: Add data_root as new MMR leaf
PN->>S: Update MMR root
PN->>PN: Sign commitment payload
Note over PN: CommitmentPayload {
Note over PN: bucket_id,
Note over PN: mmr_root,
Note over PN: start_seq,
Note over PN: leaf_count: 0
Note over PN: }
PN-->>SC: { mmr_root, start_seq, leaf_indices, provider_signature }
SC-->>U: data_root (CID)
This is how off-chain state becomes on-chain:
sequenceDiagram
participant U as User
participant SC as Storage Client
participant PN as Provider Node(s)
participant C as Chain (Pallet)
Note over U,C: Step 1: Collect signatures from providers
loop For each primary provider
SC->>PN: GET /commitment?bucket_id=X
PN->>PN: Sign CommitmentPayload
PN-->>SC: { mmr_root, start_seq, provider_signature }
end
Note over SC: Verify all providers agree on same mmr_root
Note over U,C: Step 2: Submit checkpoint on-chain
U->>C: submit_commitment(bucket_id, mmr_root, start_seq, leaf_count, signatures[])
Note over C: signatures = [(provider1, sig1), (provider2, sig2), ...]
C->>C: bucket = Buckets::get(bucket_id)?
loop For each (provider, signature)
Note over C: Verify provider is in bucket
C->>C: idx = bucket.primary_providers.position(provider)?
Note over C: Build payload
C->>C: payload = CommitmentPayload::new(bucket_id, mmr_root, start_seq, leaf_count)
Note over C: Verify signature against provider's public key
C->>C: provider_info = Providers::get(provider)?
C->>C: verify_signature(signature, payload.encode(), provider_info.public_key)?
Note over C: Mark provider as signed (bitfield)
C->>C: primary_signers[idx / 8] |= 1 << (idx % 8)
end
Note over C: Check threshold (51% of providers)
C->>C: ensure!(signing_count >= bucket.min_providers * 51%)
Note over C: Create/update snapshot
C->>C: bucket.snapshot = Some(BucketSnapshot {
Note over C: mmr_root,
Note over C: start_seq,
Note over C: leaf_count,
Note over C: checkpoint_block: current_block,
Note over C: primary_signers
Note over C: })
C-->>U: Event::CommitmentSubmitted { bucket_id, mmr_root, signers }
┌─────────────────────────────────────────────────────────────────────────┐
│ Signature Verification Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Provider registers with public_key │
│ Providers::insert(provider_id, { public_key, ... }) │
│ │
│ 2. Provider signs commitment off-chain │
│ signature = sr25519_sign(private_key, CommitmentPayload.encode()) │
│ │
│ 3. On-chain verification │
│ sr25519_verify(signature, payload, stored_public_key) │
│ │
│ This ensures: │
│ • Only the registered provider could have signed │
│ • Provider agreed to store this specific data (mmr_root) │
│ • Provider can be held accountable (challenged/slashed) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
sequenceDiagram
participant U as User
participant SC as Storage Client
participant PN as Provider Node
participant S as Storage Layer
U->>SC: read(data_root, offset, length)
SC->>PN: GET /read?data_root=0x...&offset=0&length=1000000
Note over PN: Calculate which chunks needed
PN->>PN: start_chunk = offset / 256KB
PN->>PN: end_chunk = (offset + length) / 256KB
loop For each chunk index
PN->>S: get_chunk_at_index(data_root, chunk_idx)
S-->>PN: (chunk_data, merkle_proof)
end
PN-->>SC: { chunks: [{ hash, data, proof }, ...] }
Note over SC: Verify each chunk
loop For each chunk
SC->>SC: actual_hash = blake2_256(chunk_data)
SC->>SC: ensure!(actual_hash == expected_hash)
SC->>SC: verify_merkle_proof(hash, proof, data_root)
end
SC->>SC: Reassemble data from chunks
SC->>SC: Trim to requested range [offset, offset+length]
SC-->>U: data bytes
When a user suspects data loss:
sequenceDiagram
participant U as Challenger
participant C as Chain (Pallet)
participant P as Provider
U->>C: challenge_checkpoint(bucket_id, provider, leaf_index, chunk_index)
Note over C: Verify provider signed the snapshot
C->>C: bucket = Buckets::get(bucket_id)?
C->>C: snapshot = bucket.snapshot?
C->>C: provider_idx = bucket.primary_providers.position(provider)?
C->>C: ensure!(snapshot.has_provider_signed(provider_idx))
Note over C: Create challenge
C->>C: deadline = current_block + ChallengePeriod
C->>C: challenge = Challenge {
Note over C: challenger,
Note over C: bucket_id,
Note over C: provider,
Note over C: mmr_root: snapshot.mmr_root,
Note over C: start_seq: snapshot.start_seq,
Note over C: leaf_index,
Note over C: chunk_index,
Note over C: deposit
Note over C: }
C->>C: Challenges::append(deadline, challenge)
C-->>U: Event::ChallengeCreated { challenge_id, deadline }
C-->>P: Event::ChallengeCreated { ... } // Provider monitors events
Provider must prove they have the data:
sequenceDiagram
participant P as Provider
participant PN as Provider Node
participant C as Chain (Pallet)
Note over P: Provider detects challenge event
P->>PN: GET /mmr_proof?bucket_id=X&leaf_index=Y
PN-->>P: { leaf: { data_root, data_size }, peaks, proof }
P->>PN: GET /chunk_proof?data_root=0x...&chunk_index=Z
PN-->>P: { chunk_hash, proof }
P->>PN: GET /node?hash=<chunk_hash>
PN-->>P: { data: <actual chunk bytes> }
P->>C: respond_to_challenge(challenge_id, response)
Note over C: response = ChallengeResponse::Proof {
Note over C: chunk_data,
Note over C: chunk_proof, // Merkle proof chunk → data_root
Note over C: mmr_proof // MMR proof data_root → mmr_root
Note over C: }
Note over C: Verify proofs
C->>C: chunk_hash = blake2_256(chunk_data)
C->>C: verify_merkle_proof(chunk_hash, chunk_proof, data_root)?
C->>C: verify_mmr_proof(mmr_proof, mmr_root)?
Note over C: Challenge defended!
C->>C: Remove challenge
C->>C: Return challenger's deposit
C-->>P: Event::ChallengeDefended { challenge_id }
sequenceDiagram
participant C as Chain (Pallet)
participant B as Balances
Note over C: on_finalize(block_number) hook
C->>C: expired = Challenges::take(block_number)
loop For each expired challenge
Note over C: Provider failed to respond!
C->>C: Slash provider stake
C->>B: Currency::slash(provider, slash_amount)
C->>C: Reward challenger
C->>B: Currency::transfer(slash, challenger)
C->>C: Remove provider from bucket
C->>C: bucket.primary_providers.remove(provider)
C->>C: End storage agreement
C->>C: StorageAgreements::remove((bucket_id, provider))
C-->>C: Event::ProviderSlashed { provider, amount }
end
sequenceDiagram
participant U as User
participant DR as Drive Registry Pallet
participant SP as Storage Provider Pallet
participant B as Balances
U->>DR: create_drive(name, max_capacity, storage_period, payment, min_providers, commit_strategy)
Note over DR: Validate inputs
DR->>DR: ensure!(max_capacity > 0)
DR->>DR: ensure!(storage_period > 0)
DR->>DR: ensure!(payment > 0)
Note over DR: Auto-determine provider count if not specified
DR->>DR: num_providers = min_providers.unwrap_or(
DR->>DR: if storage_period > 1000 { 3 } else { 1 }
DR->>DR: )
Note over DR: Create bucket via Layer 0
DR->>SP: create_bucket(is_private: true, min_providers)
SP-->>DR: bucket_id
Note over DR: Find available providers
DR->>SP: query_available_providers(max_capacity)
SP-->>DR: [provider1, provider2, provider3]
Note over DR: Request agreements with each provider
loop For each provider
DR->>SP: request_agreement(bucket_id, provider, max_capacity, storage_period, payment/n)
DR->>SP: [Provider accepts via accept_agreement]
end
Note over DR: Create empty root directory
DR->>DR: root_dir = DirectoryNode::new_empty(drive_id)
DR->>DR: root_cid = compute_cid(root_dir.encode())
Note over DR: Store drive info
DR->>DR: drive = DriveInfo {
Note over DR: owner,
Note over DR: bucket_id,
Note over DR: root_cid,
Note over DR: commit_strategy,
Note over DR: created_at: current_block,
Note over DR: ...
Note over DR: }
DR->>DR: Drives::insert(drive_id, drive)
DR->>DR: UserDrives::append(owner, drive_id)
DR->>DR: BucketToDrive::insert(bucket_id, drive_id)
DR-->>U: Event::DriveCreated { drive_id, bucket_id, root_cid }
sequenceDiagram
participant U as User
participant FSC as File System Client
participant PN as Provider Node
participant DR as Drive Registry Pallet
Note over U,DR: After file operations, root CID changes
U->>FSC: upload_file(drive_id, "/docs/report.pdf", data)
Note over FSC: Update directory tree
FSC->>PN: Upload file chunks
FSC->>PN: Upload file manifest
FSC->>PN: Upload updated /docs directory
FSC->>PN: Upload updated / root directory
FSC->>PN: POST /commit (get signature)
PN-->>FSC: new_root_cid, provider_signature
Note over FSC: Based on CommitStrategy
alt Immediate
FSC->>DR: update_root_cid(drive_id, new_root_cid)
else Batched
FSC->>FSC: Queue update, submit on interval
else Manual
FSC->>FSC: Store pending, wait for user
end
U->>DR: update_root_cid(drive_id, new_root_cid)
DR->>DR: drive = Drives::get(drive_id)?
DR->>DR: ensure!(drive.owner == caller)
DR->>DR: old_cid = drive.root_cid
DR->>DR: drive.root_cid = new_root_cid
DR->>DR: drive.last_committed_at = current_block
DR->>DR: Drives::insert(drive_id, drive)
DR-->>U: Event::RootCIDUpdated { drive_id, old_cid, new_root_cid }
┌─────────────────────────────────────────────────────────────────────────┐
│ Why Signatures at Each Step │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Provider Registration │
│ └─ Provider registers public_key on-chain │
│ └─ Establishes identity for signature verification │
│ │
│ 2. Off-chain Commit │
│ └─ Provider signs CommitmentPayload │
│ └─ Client stores signature as proof of provider's agreement │
│ │
│ 3. On-chain Checkpoint │
│ └─ Client submits provider signatures │
│ └─ Chain verifies each signature against provider's public_key │
│ └─ Creates non-repudiable record of what provider claimed to store │
│ │
│ 4. Challenge │
│ └─ Anyone can challenge providers who signed the checkpoint │
│ └─ Signature proves provider agreed to be accountable │
│ └─ Provider must prove data or lose stake │
│ │
│ 5. Off-chain Challenge (challenge_offchain) │
│ └─ For data not yet checkpointed on-chain │
│ └─ Client provides provider's signature from /commit response │
│ └─ Chain verifies signature, creates challenge │
│ │
│ Result: Signatures create a chain of accountability │
│ Provider → "I have this data" (signature) │
│ Chain → "Prove it or lose stake" (challenge) │
│ Provider → "Here's the proof" OR → Slashed │
│ │
└─────────────────────────────────────────────────────────────────────────┘
- Layer 0 Extrinsics Reference - Complete pallet API
- Layer 1 API Reference - File System API
- Architecture Overview - System architecture
- Admin Guide - System administration
Last updated: February 2026