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:
// 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 messageThe 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
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 limitThe Frontend: React + Web3
Encryption Flow
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
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:
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:
# 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:
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:
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:
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:
# 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
// 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:
- Mainnet Deployment: Take it to production
- Social Features: Share capsules with friends
- Encrypted Media: Support images and videos
- NFT Integration: Turn capsules into collectibles
Want to try Time Capsule Protocol? Visit time-capsule-protocol.vercel.app