Skip to content

Latest commit

 

History

History
717 lines (546 loc) · 28 KB

File metadata and controls

717 lines (546 loc) · 28 KB

Extrinsic Execution Flows

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.

Table of Contents

  1. Overview
  2. Why Checkpoints Require Provider Signatures
  3. Provider Registration
  4. Bucket Creation
  5. Storage Agreements
  6. Data Upload Flow
  7. Checkpoint (Commitment) Flow
  8. Data Read Flow
  9. Challenge Flow
  10. Layer 1: Drive Operations

Overview

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                                         │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Why Checkpoints Require Provider Signatures

The Problem

When a client uploads data to a provider, how do we ensure the provider actually stores it? The provider could:

  1. Accept the data, discard it, and claim storage payment
  2. Store it initially but delete it later
  3. Serve data only when convenient

The Solution: Signed Commitments

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                            │
└─────────────────────────────────────────────────────────────────────────┘

Why Not Just Trust the Client?

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

Multi-Provider Checkpoints

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!           │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Provider Registration

Extrinsic: register_provider

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

Extrinsic: update_provider_settings

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

Bucket Creation

Extrinsic: create_bucket

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

Storage Agreements

Extrinsic: request_agreement

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

Extrinsic: accept_agreement

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

Data Upload Flow

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

Checkpoint (Commitment) Flow

Extrinsic: submit_commitment

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

Why Signature Verification Matters

┌─────────────────────────────────────────────────────────────────────────┐
│  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)                │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Data Read Flow

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
Loading

Challenge Flow

Extrinsic: challenge_checkpoint

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
Loading

Extrinsic: respond_to_challenge

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

Automatic Slashing (if no response)

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
Loading

Layer 1: Drive Operations

Extrinsic: create_drive (Drive Registry Pallet)

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

Extrinsic: update_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 }
Loading

Summary: Signature Role in the System

┌─────────────────────────────────────────────────────────────────────────┐
│  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                             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

API Reference Links


Last updated: February 2026