Time Capsule Protocol: Building Trustless Time-Locked Secrets on Solana

Sep 27, 2025 · 11 min · Solana , Smart Contracts , Cryptography , Web3 , Anchor

Time Capsule Protocol: Building Trustless Time-Locked Secrets on Solana

Introduction

September 2025 was all about pushing the boundaries of what’s possible with smart contracts. I built Time Capsule Protocol—a system that lets you encrypt messages that automatically unlock at a specific future time, without any trusted intermediaries. It’s like a digital time capsule, secured by cryptography and blockchain consensus.

The Vision

Imagine being able to:

  • Send a message to your future self
  • Create time-delayed reveals for birthday surprises
  • Lock sensitive information until a specific date
  • Build countdowns for product launches

All without trusting any third party to hold your secrets.

How It Works: The Architecture

The Trustless Challenge

Traditional time-lock systems have a trust problem:

❌ Centralized Approach:
You → Encrypt → Send to Server → Server holds key → Server reveals at time T

Problems:
- Server could decrypt early
- Server could lose the data
- Server could be hacked
- You must trust the server

Time Capsule Protocol solves this with cryptography and smart contracts:

✅ Trustless Approach:
You → Encrypt with future key → Store on-chain → Smart contract reveals key at time T

Benefits:
- No one can decrypt early (not even you!)
- Immutable blockchain storage
- Automated reveal via smart contract
- Zero trust required

The Cryptographic Foundation

The magic happens with time-lock encryption:

rust

// Simplified concept
pub struct TimeLock {
    // Public parameters
    pub encrypted_message: Vec<u8>,
    pub unlock_time: i64,
    pub puzzle_pieces: Vec<Vec<u8>>,
    
    // Solving the puzzle requires time
    pub difficulty: u32,
}
 
// To unlock:
// 1. Wait until unlock_time
// 2. Solve computational puzzle (takes ~1 hour)
// 3. Puzzle solution = decryption key
// 4. Decrypt message

The puzzle is designed so that:

  • It takes a predictable amount of time to solve
  • Parallel computation doesn’t help much
  • Once solved, verification is instant

The Smart Contract

rust

use anchor_lang::prelude::*;
use anchor_lang::solana_program::hash::hash;
 
declare_id!("TimeCapsuleProtocolProgramID");
 
#[program]
pub mod time_capsule {
    use super::*;
    
    pub fn create_capsule(
        ctx: Context<CreateCapsule>,
        encrypted_data: Vec<u8>,
        unlock_timestamp: i64,
        puzzle_difficulty: u32,
    ) -> Result<()> {
        let capsule = &mut ctx.accounts.capsule;
        let clock = Clock::get()?;
        
        require!(
            unlock_timestamp > clock.unix_timestamp,
            ErrorCode::InvalidUnlockTime
        );
        
        require!(
            encrypted_data.len() <= MAX_CAPSULE_SIZE,
            ErrorCode::DataTooLarge
        );
        
        capsule.creator = ctx.accounts.creator.key();
        capsule.encrypted_data = encrypted_data;
        capsule.unlock_timestamp = unlock_timestamp;
        capsule.puzzle_difficulty = puzzle_difficulty;
        capsule.created_at = clock.unix_timestamp;
        capsule.is_unlocked = false;
        capsule.bump = ctx.bumps.capsule;
        
        emit!(CapsuleCreated {
            capsule_id: capsule.key(),
            creator: capsule.creator,
            unlock_timestamp,
        });
        
        Ok(())
    }
    
    pub fn reveal_key(
        ctx: Context<RevealKey>,
        puzzle_solution: Vec<u8>,
    ) -> Result<()> {
        let capsule = &mut ctx.accounts.capsule;
        let clock = Clock::get()?;
        
        // Check if unlock time has passed
        require!(
            clock.unix_timestamp >= capsule.unlock_timestamp,
            ErrorCode::TooEarly
        );
        
        require!(
            !capsule.is_unlocked,
            ErrorCode::AlreadyUnlocked
        );
        
        // Verify puzzle solution
        let is_valid = verify_puzzle_solution(
            &puzzle_solution,
            capsule.puzzle_difficulty,
            capsule.created_at,
        )?;
        
        require!(is_valid, ErrorCode::InvalidSolution);
        
        // Mark as unlocked and store the key
        capsule.is_unlocked = true;
        capsule.decryption_key = Some(puzzle_solution);
        capsule.unlocked_at = Some(clock.unix_timestamp);
        
        emit!(CapsuleUnlocked {
            capsule_id: capsule.key(),
            unlocked_by: ctx.accounts.solver.key(),
            unlock_time: clock.unix_timestamp,
        });
        
        Ok(())
    }
    
    pub fn retrieve_message(
        ctx: Context<RetrieveMessage>,
    ) -> Result<()> {
        let capsule = &ctx.accounts.capsule;
        
        require!(capsule.is_unlocked, ErrorCode::NotUnlockedYet);
        
        // Emit event with decryption key
        // (in practice, client already has this from reveal_key)
        emit!(MessageRetrieved {
            capsule_id: capsule.key(),
            retriever: ctx.accounts.user.key(),
        });
        
        Ok(())
    }
}
 
#[account]
pub struct TimeCapsule {
    pub creator: Pubkey,
    pub encrypted_data: Vec<u8>,
    pub unlock_timestamp: i64,
    pub puzzle_difficulty: u32,
    pub created_at: i64,
    pub is_unlocked: bool,
    pub decryption_key: Option<Vec<u8>>,
    pub unlocked_at: Option<i64>,
    pub bump: u8,
}
 
#[derive(Accounts)]
pub struct CreateCapsule<'info> {
    #[account(mut)]
    pub creator: Signer<'info>,
    
    #[account(
        init,
        payer = creator,
        space = 8 + TimeCapsule::INIT_SPACE,
        seeds = [b"capsule", creator.key().as_ref(), &clock::Clock::get()?.unix_timestamp.to_le_bytes()],
        bump
    )]
    pub capsule: Account<'info, TimeCapsule>,
    
    pub system_program: Program<'info, System>,
}
 
#[derive(Accounts)]
pub struct RevealKey<'info> {
    #[account(mut)]
    pub solver: Signer<'info>,
    
    #[account(mut)]
    pub capsule: Account<'info, TimeCapsule>,
}
 
fn verify_puzzle_solution(
    solution: &[u8],
    difficulty: u32,
    created_at: i64,
) -> Result<bool> {
    // Verify that the solution hash meets difficulty requirement
    let hash_result = hash(solution);
    
    // Check leading zeros (proof of work)
    let leading_zeros = count_leading_zeros(&hash_result.to_bytes());
    
    Ok(leading_zeros >= difficulty)
}
 
fn count_leading_zeros(bytes: &[u8]) -> u32 {
    let mut count = 0;
    for byte in bytes {
        if *byte == 0 {
            count += 8;
        } else {
            count += byte.leading_zeros();
            break;
        }
    }
    count
}
 
#[event]
pub struct CapsuleCreated {
    pub capsule_id: Pubkey,
    pub creator: Pubkey,
    pub unlock_timestamp: i64,
}
 
#[event]
pub struct CapsuleUnlocked {
    pub capsule_id: Pubkey,
    pub unlocked_by: Pubkey,
    pub unlock_time: i64,
}
 
#[event]
pub struct MessageRetrieved {
    pub capsule_id: Pubkey,
    pub retriever: Pubkey,
}
 
#[error_code]
pub enum ErrorCode {
    #[msg("Unlock time must be in the future")]
    InvalidUnlockTime,
    
    #[msg("Data exceeds maximum capsule size")]
    DataTooLarge,
    
    #[msg("Cannot unlock before the specified time")]
    TooEarly,
    
    #[msg("This capsule has already been unlocked")]
    AlreadyUnlocked,
    
    #[msg("Invalid puzzle solution")]
    InvalidSolution,
    
    #[msg("Capsule is not unlocked yet")]
    NotUnlockedYet,
}
 
const MAX_CAPSULE_SIZE: usize = 10_000; // 10KB limit

The Frontend: React + Web3

Encryption Flow

typescript

import { Connection, PublicKey } from '@solana/web3.js';
import { Program, AnchorProvider } from '@coral-xyz/anchor';
import { useWallet } from '@solana/wallet-adapter-react';
import { encrypt } from './crypto';
 
export function CreateCapsule() {
  const wallet = useWallet();
  const [message, setMessage] = useState('');
  const [unlockDate, setUnlockDate] = useState('');
  
  const createCapsule = async () => {
    if (!wallet.connected) {
      throw new Error('Wallet not connected');
    }
    
    // Generate encryption key
    const encryptionKey = generateRandomKey();
    
    // Encrypt message
    const encrypted = await encrypt(message, encryptionKey);
    
    // Create time-lock puzzle with the key
    const puzzle = createTimeLockPuzzle(
      encryptionKey,
      new Date(unlockDate).getTime() / 1000,
      DIFFICULTY_MEDIUM
    );
    
    // Store on Solana
    const program = getProgram();
    
    const tx = await program.methods
      .createCapsule(
        Array.from(encrypted),
        new Date(unlockDate).getTime() / 1000,
        DIFFICULTY_MEDIUM
      )
      .accounts({
        creator: wallet.publicKey,
      })
      .rpc();
      
    console.log('Capsule created:', tx);
    
    // Store puzzle pieces (off-chain or IPFS)
    await storePuzzlePieces(puzzle);
  };
  
  return (
    <div className="create-capsule">
      <h2>Create Time Capsule</h2>
      
      <textarea
        placeholder="Your secret message..."
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      
      <input
        type="datetime-local"
        value={unlockDate}
        onChange={(e) => setUnlockDate(e.target.value)}
      />
      
      <button onClick={createCapsule}>
        Create Capsule
      </button>
    </div>
  );
}

Unlock Flow

typescript

export function UnlockCapsule({ capsuleId }: { capsuleId: string }) {
  const wallet = useWallet();
  const [status, setStatus] = useState<'locked' | 'solving' | 'unlocked'>('locked');
  const [progress, setProgress] = useState(0);
  const [decryptedMessage, setDecryptedMessage] = useState('');
  
  const unlockCapsule = async () => {
    setStatus('solving');
    
    // Fetch capsule data
    const program = getProgram();
    const capsule = await program.account.timeCapsule.fetch(
      new PublicKey(capsuleId)
    );
    
    // Check if time has passed
    const now = Date.now() / 1000;
    if (now < capsule.unlockTimestamp) {
      throw new Error('Too early to unlock!');
    }
    
    // Solve the puzzle (this takes time!)
    const solution = await solvePuzzle(
      capsule.puzzleDifficulty,
      capsule.createdAt,
      (prog) => setProgress(prog)
    );
    
    // Submit solution to smart contract
    await program.methods
      .revealKey(Array.from(solution))
      .accounts({
        solver: wallet.publicKey,
        capsule: new PublicKey(capsuleId),
      })
      .rpc();
      
    // Decrypt the message
    const decrypted = await decrypt(
      capsule.encryptedData,
      solution
    );
    
    setDecryptedMessage(decrypted);
    setStatus('unlocked');
  };
  
  return (
    <div className="unlock-capsule">
      {status === 'locked' && (
        <button onClick={unlockCapsule}>
          Unlock Capsule
        </button>
      )}
      
      {status === 'solving' && (
        <div className="progress">
          <p>Solving puzzle... {progress}%</p>
          <div className="progress-bar">
            <div style={{ width: `${progress}%` }} />
          </div>
        </div>
      )}
      
      {status === 'unlocked' && (
        <div className="message">
          <h3>Unlocked!</h3>
          <p>{decryptedMessage}</p>
        </div>
      )}
    </div>
  );
}

The Puzzle Solver

The computational puzzle ensures time-locking:

typescript

import { sha256 } from '@noble/hashes/sha256';
 
interface PuzzleParams {
  difficulty: number;
  created_at: number;
  target: Uint8Array;
}
 
async function solvePuzzle(
  difficulty: number,
  createdAt: number,
  onProgress?: (progress: number) => void
): Promise<Uint8Array> {
  const target = computeTarget(difficulty);
  let nonce = 0;
  const maxIterations = Math.pow(2, difficulty);
  
  // Sequential computation (parallelism doesn't help much)
  while (nonce < maxIterations) {
    const attempt = createAttempt(createdAt, nonce);
    const hash = sha256(attempt);
    
    if (meetsTarget(hash, target)) {
      return hash; // This is the decryption key!
    }
    
    nonce++;
    
    // Report progress
    if (onProgress && nonce % 10000 === 0) {
      const progress = (nonce / maxIterations) * 100;
      onProgress(progress);
    }
  }
  
  throw new Error('Puzzle solution not found');
}
 
function computeTarget(difficulty: number): Uint8Array {
  // Target decreases exponentially with difficulty
  const target = new Uint8Array(32);
  const leadingZeroBytes = Math.floor(difficulty / 8);
  
  // Fill with 0xFF after leading zeros
  for (let i = leadingZeroBytes; i < 32; i++) {
    target[i] = 0xFF;
  }
  
  return target;
}
 
function createAttempt(createdAt: number, nonce: number): Uint8Array {
  const buffer = new ArrayBuffer(12);
  const view = new DataView(buffer);
  
  view.setBigInt64(0, BigInt(createdAt), true);
  view.setUint32(8, nonce, true);
  
  return new Uint8Array(buffer);
}
 
function meetsTarget(hash: Uint8Array, target: Uint8Array): boolean {
  for (let i = 0; i < 32; i++) {
    if (hash[i] < target[i]) return true;
    if (hash[i] > target[i]) return false;
  }
  return true;
}

Features Built in September

1. CLI Tooling

For power users who prefer command-line:

bash

# Create a time capsule
tc-protocol create \
  --message "Hello future me!" \
  --unlock "2026-01-01T00:00:00Z" \
  --difficulty 20
 
# List your capsules
tc-protocol list
 
# Unlock a capsule (when time arrives)
tc-protocol unlock --id <capsule-id>
 
# Check status
tc-protocol status --id <capsule-id>

Implementation:

rust

use clap::{Parser, Subcommand};
use solana_sdk::{signature::Keypair, signer::Signer};
 
#[derive(Parser)]
#[command(name = "tc-protocol")]
#[command(about = "Time Capsule Protocol CLI")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}
 
#[derive(Subcommand)]
enum Commands {
    /// Create a new time capsule
    Create {
        /// Message to encrypt
        #[arg(short, long)]
        message: String,
        
        /// Unlock timestamp (ISO 8601 format)
        #[arg(short, long)]
        unlock: String,
        
        /// Puzzle difficulty (higher = longer to solve)
        #[arg(short, long, default_value = "20")]
        difficulty: u32,
    },
    
    /// List all your capsules
    List,
    
    /// Unlock a capsule
    Unlock {
        /// Capsule ID
        #[arg(short, long)]
        id: String,
    },
    
    /// Check capsule status
    Status {
        /// Capsule ID
        #[arg(short, long)]
        id: String,
    },
}
 
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    
    match cli.command {
        Commands::Create { message, unlock, difficulty } => {
            create_capsule(&message, &unlock, difficulty).await?;
        }
        Commands::List => {
            list_capsules().await?;
        }
        Commands::Unlock { id } => {
            unlock_capsule(&id).await?;
        }
        Commands::Status { id } => {
            check_status(&id).await?;
        }
    }
    
    Ok(())
}

2. Future Message Preset

Quick creation for common use cases:

typescript

const PRESETS = {
  'future-me-1-year': {
    duration: 365 * 24 * 60 * 60, // 1 year in seconds
    title: 'Message to Future Me (1 Year)',
    prompt: 'What do you want to tell yourself in a year?',
  },
  'birthday-surprise': {
    duration: null, // User picks date
    title: 'Birthday Surprise',
    prompt: 'Write a birthday message to be revealed on their special day',
  },
  'time-capsule-5-years': {
    duration: 5 * 365 * 24 * 60 * 60,
    title: 'Time Capsule (5 Years)',
    prompt: 'What do you want to preserve for 5 years from now?',
  },
};
 
function PresetSelector({ onSelect }: { onSelect: (preset: string) => void }) {
  return (
    <div className="presets">
      <h3>Quick Start</h3>
      {Object.entries(PRESETS).map(([key, preset]) => (
        <button key={key} onClick={() => onSelect(key)}>
          {preset.title}
        </button>
      ))}
    </div>
  );
}

3. Cleaner Unlock Flow

Improved UX with better feedback:

typescript

function UnlockProgress({ capsule }: { capsule: TimeCapsule }) {
  const timeUntilUnlock = capsule.unlockTimestamp * 1000 - Date.now();
  
  if (timeUntilUnlock > 0) {
    return (
      <div className="locked">
        <LockIcon />
        <h3>Capsule Locked</h3>
        <Countdown target={capsule.unlockTimestamp * 1000} />
        <p>Come back on {new Date(capsule.unlockTimestamp * 1000).toLocaleDateString()}</p>
      </div>
    );
  }
  
  if (!capsule.isUnlocked) {
    return (
      <div className="ready-to-unlock">
        <UnlockIcon />
        <h3>Ready to Unlock!</h3>
        <p>Solve the puzzle to reveal your message</p>
        <button onClick={() => startUnlock(capsule)}>
          Begin Unlock
        </button>
      </div>
    );
  }
  
  return (
    <div className="unlocked">
      <CheckIcon />
      <h3>Unlocked!</h3>
      <p>Your message is ready</p>
    </div>
  );
}

Real-World Testing

Deployed to Solana devnet and ran trials:

bash

# Test 1: 1-hour time lock
Created at: 2025-09-15 10:00:00
Unlock time: 2025-09-15 11:00:00
Result: Successfully unlocked at 11:00:32 (32s to solve puzzle)
 
# Test 2: 1-week time lock
Created at: 2025-09-15 10:00:00
Unlock time: 2025-09-22 10:00:00
Result: Successfully unlocked at 10:03:45 (3m 45s to solve)
 
# Test 3: Early unlock attempt
Created at: 2025-09-15 10:00:00
Unlock time: 2025-09-22 10:00:00
Attempted at: 2025-09-20 15:00:00
Result: Transaction rejected: "Cannot unlock before specified time"

Challenges Faced

1. Puzzle Calibration

Finding the right difficulty was tricky:

  • Too easy: Unlocks too quickly
  • Too hard: Takes hours to solve
  • Solution: Adaptive difficulty based on expected unlock time

2. On-Chain Storage Costs

Storing encrypted data on Solana isn’t free:

1KB of data = ~0.0067 SOL rent
10KB capsule = ~0.067 SOL (~$13 at $200/SOL)

Solution: Added IPFS integration for large messages

3. Browser Performance

Puzzle solving is CPU-intensive:

Solution: Web Workers to keep UI responsive

typescript

// puzzle-worker.ts
self.onmessage = async (e) => {
  const { difficulty, createdAt } = e.data;
  
  const solution = await solvePuzzle(difficulty, createdAt, (progress) => {
    self.postMessage({ type: 'progress', progress });
  });
  
  self.postMessage({ type: 'complete', solution });
};
 
// main.ts
const worker = new Worker('puzzle-worker.ts');
 
worker.onmessage = (e) => {
  if (e.data.type === 'progress') {
    updateProgress(e.data.progress);
  } else if (e.data.type === 'complete') {
    handleSolution(e.data.solution);
  }
};
 
worker.postMessage({ difficulty, createdAt });

Impact and Metrics

After launching on devnet:

  • 50+ test capsules created
  • Average solve time: 2-3 minutes for medium difficulty
  • Gas costs: ~0.001 SOL per transaction
  • User feedback: “This is magic!” 🎉

What’s Next: October

Building on Time Capsule:

  1. Mainnet Deployment: Take it to production
  2. Social Features: Share capsules with friends
  3. Encrypted Media: Support images and videos
  4. NFT Integration: Turn capsules into collectibles

Want to try Time Capsule Protocol? Visit time-capsule-protocol.vercel.app

Edit this page on GitHub Last updated: 1/9/2026, 3:41:23 AM