Hybrid Architectures

Understanding how to build partially centralized systems that leverage both Web2 efficiency and Web3 trustlessness.

Why Hybrid Architectures?

Not every operation needs to be on-chain. Hybrid architectures combine:

  • On-chain: Settlement, ownership, trustless execution
  • Off-chain: Speed, privacy, complex computation
Text
┌─────────────────────────────────────────────────────────────────┐
│                    Hybrid Architecture Spectrum                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Fully            Partially           Fully                     │
│  Centralized      Centralized         Decentralized             │
│  ◄────────────────────┬────────────────────►                    │
│                       │                                         │
│  Traditional     Hybrid dApps      Pure On-chain                │
│  Web2 Apps       (This Chapter)    Protocols                    │
│                                                                 │
│  Examples:       Examples:          Examples:                   │
│  - Banks         - Exchanges        - Uniswap                   │
│  - Social Media  - Games            - Aave                      │
│                  - NFT platforms    - MakerDAO                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Common Patterns

1. Off-chain Ordering, On-chain Settlement

Many exchanges use this pattern for performance:

TypeScript
// Off-chain: Order matching in backend
interface Order {
  user: string;
  side: "buy" | "sell";
  price: number;
  quantity: number;
  signature: string; // User signed this order
}

class OffChainOrderBook {
  private orders: Order[] = [];
  
  async addOrder(order: Order) {
    // Verify signature
    const isValid = await verifySignature(order);
    if (!isValid) throw new Error("Invalid signature");
    
    // Match orders off-chain
    const matches = this.findMatches(order);
    
    // Settle matched orders on-chain
    for (const match of matches) {
      await this.settleOnChain(order, match);
    }
  }
  
  async settleOnChain(order1: Order, order2: Order) {
    // Submit to Solana program for atomic swap
    const tx = await program.methods
      .settle(order1, order2)
      .accounts({
        user1: new PublicKey(order1.user),
        user2: new PublicKey(order2.user),
        // ...
      })
      .rpc();
  }
}

2. Centralized Gateway, Decentralized Core

Text
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  User Request                                                   │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────┐                                               │
│  │   API       │  ◄── Rate limiting, auth, validation          │
│  │   Gateway   │                                                │
│  └──────┬──────┘                                               │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                               │
│  │  Business   │  ◄── Complex logic, database                  │
│  │   Logic     │                                                │
│  └──────┬──────┘                                               │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────┐                                               │
│  │   Solana    │  ◄── Settlement, ownership, finality          │
│  │   Program   │                                                │
│  └─────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Merkle Root Commitments

Batch multiple operations off-chain, commit single proof on-chain:

Rust
use anchor_lang::prelude::*;

#[program]
pub mod merkle_settlement {
    use super::*;

    pub fn commit_batch(
        ctx: Context<CommitBatch>,
        merkle_root: [u8; 32],
        batch_id: u64,
    ) -> Result<()> {
        let batch = &mut ctx.accounts.batch;
        batch.merkle_root = merkle_root;
        batch.batch_id = batch_id;
        batch.timestamp = Clock::get()?.unix_timestamp;
        batch.authority = ctx.accounts.authority.key();
        Ok(())
    }

    pub fn claim_with_proof(
        ctx: Context<ClaimWithProof>,
        amount: u64,
        proof: Vec<[u8; 32]>,
        leaf_index: u32,
    ) -> Result<()> {
        let batch = &ctx.accounts.batch;
        let user = ctx.accounts.user.key();
        
        // Construct leaf
        let leaf = keccak::hashv(&[
            user.as_ref(),
            &amount.to_le_bytes(),
        ]);
        
        // Verify Merkle proof
        let computed_root = verify_proof(proof, leaf.0, leaf_index);
        require!(
            computed_root == batch.merkle_root,
            ErrorCode::InvalidProof
        );
        
        // Process claim
        // Transfer tokens, update state, etc.
        
        Ok(())
    }
}

fn verify_proof(proof: Vec<[u8; 32]>, leaf: [u8; 32], index: u32) -> [u8; 32] {
    let mut current = leaf;
    let mut idx = index;
    
    for sibling in proof {
        current = if idx % 2 == 0 {
            keccak::hashv(&[&current, &sibling]).0
        } else {
            keccak::hashv(&[&sibling, &current]).0
        };
        idx /= 2;
    }
    
    current
}

Upgrade Patterns

1. Proxy Pattern (Upgradeable Programs)

Rust
// Anchor's built-in upgrade mechanism
#[program]
pub mod upgradeable_program {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let config = &mut ctx.accounts.config;
        config.admin = ctx.accounts.admin.key();
        config.version = 1;
        Ok(())
    }
    
    // This program can be upgraded by the admin
    // using `anchor upgrade` command
}

#[account]
pub struct Config {
    pub admin: Pubkey,
    pub version: u8,
    pub paused: bool,
}

2. Timelock for Upgrades

Rust
#[account]
pub struct UpgradeProposal {
    pub proposer: Pubkey,
    pub new_program_data: Pubkey,
    pub proposed_at: i64,
    pub execution_time: i64, // Must wait this long
    pub executed: bool,
}

pub fn propose_upgrade(
    ctx: Context<ProposeUpgrade>,
    new_program_data: Pubkey,
) -> Result<()> {
    let proposal = &mut ctx.accounts.proposal;
    
    proposal.proposer = ctx.accounts.proposer.key();
    proposal.new_program_data = new_program_data;
    proposal.proposed_at = Clock::get()?.unix_timestamp;
    proposal.execution_time = proposal.proposed_at + 7 * 24 * 60 * 60; // 7 days
    proposal.executed = false;
    
    Ok(())
}

pub fn execute_upgrade(ctx: Context<ExecuteUpgrade>) -> Result<()> {
    let proposal = &ctx.accounts.proposal;
    
    require!(
        Clock::get()?.unix_timestamp >= proposal.execution_time,
        ErrorCode::TimelockNotExpired
    );
    require!(!proposal.executed, ErrorCode::AlreadyExecuted);
    
    // Execute the upgrade via BPF loader
    // ...
    
    Ok(())
}

Emergency Controls

Circuit Breaker Pattern

Rust
#[account]
pub struct ProtocolState {
    pub admin: Pubkey,
    pub paused: bool,
    pub pause_guardian: Pubkey, // Can pause but not unpause
    pub last_pause_time: i64,
}

pub fn pause(ctx: Context<Pause>) -> Result<()> {
    let state = &mut ctx.accounts.state;
    
    require!(
        ctx.accounts.signer.key() == state.admin ||
        ctx.accounts.signer.key() == state.pause_guardian,
        ErrorCode::Unauthorized
    );
    
    state.paused = true;
    state.last_pause_time = Clock::get()?.unix_timestamp;
    
    emit!(ProtocolPaused {
        paused_by: ctx.accounts.signer.key(),
        timestamp: state.last_pause_time,
    });
    
    Ok(())
}

pub fn unpause(ctx: Context<Unpause>) -> Result<()> {
    let state = &mut ctx.accounts.state;
    
    // Only admin can unpause
    require!(
        ctx.accounts.admin.key() == state.admin,
        ErrorCode::Unauthorized
    );
    
    state.paused = false;
    
    Ok(())
}

// Use in other instructions
#[access_control(not_paused(&ctx.accounts.state))]
pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
    // ...
}

fn not_paused(state: &ProtocolState) -> Result<()> {
    require!(!state.paused, ErrorCode::ProtocolPaused);
    Ok(())
}

Trust Assumptions

When building hybrid systems, clearly document trust assumptions:

ComponentTrust LevelFailure Mode
Smart ContractTrustlessCode bugs only
Backend APITrusted operatorAvailability, censorship
DatabaseTrusted operatorData integrity
OracleTrusted third partyIncorrect data
MultisigM-of-N signersCollusion

Best Practices

1. Minimize Trust Surface

Text
Good: Users can always withdraw directly from contract
Bad: Withdrawals require backend approval

2. Verifiable Off-chain Actions

Text
Good: Off-chain data has Merkle proofs
Bad: Off-chain data with no verification

3. Gradual Decentralization

Text
Phase 1: Admin controls upgrades
Phase 2: Timelock on upgrades
Phase 3: DAO governance
Phase 4: Immutable contracts

4. Transparent Operations

Rust
// Emit events for all significant actions
#[event]
pub struct AdminAction {
    pub admin: Pubkey,
    pub action: String,
    pub timestamp: i64,
}

Key Takeaways

  1. Hybrid architectures balance performance with decentralization
  2. Off-chain matching with on-chain settlement is common
  3. Merkle proofs enable batched verification
  4. Timelocks give users time to react to changes
  5. Circuit breakers provide emergency controls
  6. Trust assumptions should be explicit and minimized

Previous: Multisig Operations