← Back to Proposals
On-Chain Shard Map v2 PROPOSAL v2
Implementation plan, codebase impact assessment, and blind rotation via Solana program · April 2026
What changed from v1: This version adds a full codebase impact assessment, confirms the existing Anchor program needs ZERO changes, details the two rotation strategies (Option A: client-initiated, Option D: smart-contract blind swap), and provides a parallel development plan for testing Option D on DevNet alongside ongoing feature development.
1. Codebase Impact Assessment
Complete audit results
Every file in the ShardKeep/Citadel codebase was audited for impact. The on-chain shard map architecture affects only the shard coordination layer — the vault program, wallet flows, and browser extension UI are untouched.
| Component | File(s) | Current State | Impact | Effort |
| Anchor Program (Rust) |
shardkeep_vault/lib.rs |
Single-account vault, 333 lines, 4 instructions |
NONE |
0 hrs |
| Solana Bridge CLI |
solana-bridge.js |
Vault CRUD client, 592 lines |
NONE |
0 hrs |
| Vault CRUD APIs |
solana-{store,retrieve,list,delete}.php |
HTTP proxy to solana-bridge |
NONE |
0 hrs |
| Wallet Connect |
wallet-connect.js |
Wallet Standard auth, 232 lines |
NONE |
0 hrs |
| ShardService.php |
includes/ShardService.php |
DB-backed shard location, 439 lines |
MAJOR |
20-30 hrs |
| Shards API |
api/shards.php |
Store/retrieve/delete handlers |
MAJOR |
10-15 hrs |
| Database Schema |
migrations/002_shards.sql |
citadel_shards table with node_id |
MAJOR |
5-8 hrs |
| WSS Coordinator |
Python WSS service |
Polls DB for pending shards |
MODERATE |
10-15 hrs |
| Extension storage.js |
storage.js |
Tier 2 shard stub (not implemented) |
MINOR |
5-8 hrs |
| New Dependencies |
package.json |
@coral-xyz/anchor only |
MODERATE |
2-4 hrs |
| Blind Swap Program |
New Solana program |
Does not exist yet |
NEW |
80-120 hrs |
Option A total (client-initiated rotation): 52-80 hours development + 20-30 hours testing = ~2-3 weeks
Option D total (blind swap program): Additional 80-120 hours = ~4-6 weeks on DevNet
Combined (parallel tracks): Option A ships first, Option D developed on DevNet simultaneously
What does NOT change (confirmed by audit)
- Anchor vault program (
4hfvirYMHxW4nZSuTreWRQQD45Hfc4LKmUyy3hFYcZVP) — zero modifications. The vault stores encrypted credentials; it has no knowledge of shards or shard locations.
- Vault PDA structure —
["vault", owner_pubkey] seed derivation unchanged. Entry fields (entry_id, site, username, encrypted_data, iv, timestamps) unchanged.
- Client-side encryption — AES-256-GCM encryption in popup.js unchanged. Key derivation from wallet signature unchanged.
- Wallet connect flow — Solana Wallet Standard integration unchanged. Signature-based auth unchanged.
- Heartbeat / XNode / version check — background.js node reporting unchanged.
2. Option A — Client-Initiated Rotation SHIP FIRST
How it works
Rotation happens only when the user opens the extension. Since password manager users open their extension daily (for auto-fill), this is a natural trigger.
User opens ShardKeep extension
→ Extension signs auth challenge with wallet
→ Reads user's shard map cNFTs from Solana (FREE — reads cost nothing)
→ Checks: "last rotation was N days ago, tier = Guardian (weekly)"
If rotation due:
→ Decrypts shard map locally
→ Retrieves k shards from current nodes (by shard hash, nodes don't know why)
→ Re-splits with fresh Shamir randomness (new shard set)
→ Server selects new target nodes (random, no overlap with previous)
→ Distributes new shards via WSS
→ Mints updated cNFT with new shard map (old leaf burned)
→ Server purges all shard-to-node data from memory
→ Old shards purged from previous nodes
// Server sees shard locations ONLY during the rotation window (~5-10 seconds)
// After cNFT mint + purge, server knows nothing again
Rotation schedule by tier:
| Tier | Rotation Frequency | Trigger | Cost per Entry |
| Shield (Pawn) | None (manual only) | User-initiated | $0.001 |
| Guardian (Knight) | Weekly | Extension auto-check on unlock | $0.001 × 52 = $0.052/year |
| Sentinel (Bishop/Rook) | Daily | Extension auto-check on unlock | $0.001 × 365 = $0.365/year |
| Fortress (Queen) | Daily | Extension auto-check on unlock | $0.001 × 365 = $0.365/year |
What if the user doesn't open the extension?
- 7 days without rotation: Extension shows subtle badge "Rotation overdue"
- 14+ days: Push notification (if browser allows) — "Your shards haven't been rotated. Open ShardKeep to refresh."
- 30+ days: Shards remain valid and accessible — rotation is a security enhancement, not a requirement for access. Old shard locations still work.
Security during ephemeral exposure: The server sees shard locations for ~5-10 seconds during redistribution. This is unavoidable (someone must coordinate the transfer). Mitigations:
• WSS coordinator runs in-memory only (no disk writes of shard maps)
• Shard-to-node mapping held in process RAM, purged immediately after cNFT mint confirmation
• Audit log records that rotation occurred, but NOT the shard locations
3. Option D — Blind Rotation via Solana Program DEVNET R&D
The vision: zero-knowledge rotation
A new Solana program (separate from the vault program) that coordinates shard swaps between vault nodes without any single entity seeing the full shard map.
Rotation trigger (client-initiated, same as Option A)
→ Extension reads current cNFT shard map, decrypts locally
→ Extension generates N individual swap instructions:
Instruction 1: "Node A: send blob 0xABC to Node B"
Instruction 2: "Node C: send blob 0xDEF to Node D"
Instruction 3: "Node E: send blob 0x123 to Node F"
→ Extension submits swap instructions to Solana program as a single transaction
→ On-chain program validates: signer is cNFT owner (wallet check)
→ Program emits individual swap events (each node sees ONLY its own instruction)
→ Nodes execute swaps peer-to-peer
→ Extension mints updated cNFT with new shard map
// No server is involved AT ALL
// Each node sees only: "take blob X, send to Node Y"
// No node knows the full map, who owns the blobs, or which entry they belong to
// The Solana program enforces that only the cNFT owner can issue swap instructions
What the blind swap program looks like (Anchor/Rust):
// Program: shardkeep_rotation (NEW, separate from shardkeep_vault)
// Network: DevNet for testing
#[program]
pub mod shardkeep_rotation {
use anchor_lang::prelude::*;
/// Issue a batch of shard swap instructions.
/// Only the cNFT owner (verified via wallet signature) can call this.
pub fn rotate_shards(ctx: Context<RotateShards>, swaps: Vec<SwapInstruction>) -> Result<()> {
// Verify caller owns the shard map cNFT (Bubblegum ownership check)
// Emit individual SwapEvent for each instruction
// Nodes listen for their own events via WebSocket subscription
for swap in swaps {
emit!(SwapEvent {
source_node: swap.source_node,
target_node: swap.target_node,
shard_hash: swap.shard_hash,
nonce: swap.nonce,
});
}
Ok(())
}
}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct SwapInstruction {
pub source_node: [u8; 32], // Source node public key
pub target_node: [u8; 32], // Target node public key
pub shard_hash: [u8; 32], // Blob identifier
pub nonce: u64, // Replay protection
}
#[event]
pub struct SwapEvent {
pub source_node: [u8; 32],
pub target_node: [u8; 32],
pub shard_hash: [u8; 32],
pub nonce: u64,
}
What each party sees during blind rotation:
| Party | Sees | Does NOT See |
| User's extension |
Full shard map (decrypted locally) |
— (user sees everything, it's their data) |
| Solana program |
Individual swap instructions (source → target + hash) |
Which user, which entry, full map, entry contents |
| Source vault node |
"Send blob 0xABC to Node B" |
Who owns the blob, which entry, what other nodes hold sibling shards |
| Target vault node |
"Incoming blob 0xABC from Node A" |
Who owns the blob, which entry, what other nodes hold sibling shards |
| ShardKeep servers |
Nothing |
Everything — server is not involved in Option D rotation |
| RPC indexer (Helius) |
Encrypted cNFT metadata (can't decrypt) |
Shard locations, entry contents, user identity |
Node-side: listening for swap events
Vault node subscribes to Solana WebSocket:
ws.onProgramEvent("shardkeep_rotation", filter: {source_node: MY_PUBKEY})
On SwapEvent received:
→ Look up blob by shard_hash in local storage
→ Open peer-to-peer connection to target_node
→ Transfer encrypted blob
→ Target node acks receipt
→ Source node deletes local copy
→ Both nodes emit on-chain confirmation (optional, for audit trail)
// Node never contacts ShardKeep servers during this flow
// Pure peer-to-peer, coordinated by on-chain events
4. Database Schema Changes
Migration strategy: shadow table approach
Keep the existing citadel_shards table operational during transition. Add cNFT references alongside, then phase out the node_id column once cNFT flow is validated.
-- Phase 1: Add cNFT columns to existing table
ALTER TABLE citadel_shards
ADD COLUMN nft_mint VARCHAR(44) DEFAULT NULL AFTER hmac_key,
ADD COLUMN nft_tree VARCHAR(44) DEFAULT NULL AFTER nft_mint,
ADD COLUMN nft_leaf_index BIGINT DEFAULT NULL AFTER nft_tree,
ADD COLUMN map_purged_at DATETIME DEFAULT NULL AFTER nft_leaf_index;
-- Phase 2: Shadow table for cNFT-first lookups
CREATE TABLE citadel_shard_maps (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
wallet_address VARCHAR(64) NOT NULL,
entry_id VARCHAR(64) NOT NULL,
nft_mint VARCHAR(44) NOT NULL,
nft_tree VARCHAR(44) NOT NULL,
leaf_index BIGINT NOT NULL,
security_tier ENUM('shield','guardian','sentinel','fortress') NOT NULL,
shard_count TINYINT NOT NULL,
threshold TINYINT NOT NULL,
encrypted_map VARBINARY(2048), -- Encrypted shard map (backup, also on-chain)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
rotated_at DATETIME DEFAULT NULL,
burned_at DATETIME DEFAULT NULL,
INDEX idx_wallet (wallet_address),
INDEX idx_entry (entry_id),
UNIQUE INDEX idx_mint (nft_mint)
) ENGINE=InnoDB;
-- Phase 3: After validation, drop node_id from citadel_shards
-- ALTER TABLE citadel_shards DROP COLUMN node_id;
-- (Only after Option A is fully operational and tested)
Critical: The node_id column is NOT dropped until Option A has been running successfully in production for at least 2 weeks. The shadow table allows both old and new flows to operate simultaneously during transition.
5. ShardService.php Changes (Detailed)
Three methods require modification
storeSecret() — lines 195-227
Current:
selectNodes() → INSERT citadel_shards (node_id, shard_data, ...) → WSS notify
Proposed:
selectNodes() → INSERT citadel_shards (shard_data, no node_id yet)
→ WSS distributes shards to selected nodes
→ On ack: build shard map JSON {shard_hash → node_id, ...}
→ Encrypt map with user's wallet public key
→ Mint cNFT via Bubblegum with encrypted map as metadata
→ Store nft_mint in citadel_shard_maps
→ UPDATE citadel_shards SET node_id = NULL, map_purged_at = NOW()
// node_id purged from our database after cNFT is confirmed on-chain
retrieveSecret() — lines 302-378
Current:
SELECT node_id FROM citadel_shards WHERE entry_id = ? → fetch from nodes → reconstruct
Proposed:
Client reads cNFT metadata from Solana (extension-side, not server)
→ Client decrypts shard map locally
→ Client requests shards from nodes by hash
→ Client reconstructs locally
// Server is NOT involved in retrieval at all
// This method may become a thin status-check endpoint only
deleteEntry() — lines 384-392
Current:
UPDATE citadel_shards SET deleted_at = NOW()
Proposed:
Burn the cNFT (Bubblegum burn instruction)
→ Broadcast delete to vault nodes (by shard hash)
→ UPDATE citadel_shard_maps SET burned_at = NOW()
// On-chain proof that the shard map was intentionally destroyed
6. New Dependencies Required
Node.js packages (for shard map minting)
npm install @metaplex-foundation/mpl-bubblegum \
@metaplex-foundation/mpl-token-metadata \
@metaplex-foundation/umi \
@metaplex-foundation/umi-bundle-defaults \
@solana/spl-account-compression
Anchor (for Option D blind swap program)
# New program, separate workspace
anchor init shardkeep_rotation --solana-version 1.18
# Deploy to DevNet only during R&D phase
The existing shardkeep_vault Anchor workspace and program ID are completely untouched.
7. Parallel Development Plan
Two tracks, running simultaneously
Track A: Ship to Production
Client-initiated rotation with cNFT shard maps
- Week 1-2: Merkle tree creation on DevNet. Bubblegum mint/burn integration in ShardService.php. Schema migration (shadow table).
- Week 2-3: Extension storage.js: implement Tier 2 shard store/retrieve via cNFT. Rotation auto-check on unlock.
- Week 3-4: WSS coordinator update: in-memory-only shard maps, purge after cNFT confirmation. Integration testing.
- Week 4-5: DevNet end-to-end testing. Rotation cycle validation. Edge cases (offline user, node failure during rotation).
- Week 5-6: MainNet Merkle tree. Production deployment. Monitor for 2 weeks before dropping node_id column.
Track D: DevNet R&D
Blind swap Solana program
- Week 1-3: Design shardkeep_rotation program. Define SwapInstruction and SwapEvent schemas. Write Anchor program skeleton.
- Week 3-5: Implement swap instruction handler. Ownership verification (cNFT owner check via Bubblegum). Event emission.
- Week 5-7: Build node-side WebSocket listener. Peer-to-peer blob transfer protocol. Swap ack mechanism.
- Week 7-9: Integration test on DevNet: extension issues swap instructions → program emits events → nodes execute swaps → cNFT updated.
- Week 9-10: Performance testing. Transaction cost analysis. Gas optimization. Batch swap efficiency.
- Week 10+: Security audit preparation. MainNet deployment decision based on results.
Track A ships independently. Option D is pure R&D on DevNet — it does not block any production feature. If Option D testing succeeds, it becomes an optional upgrade that replaces the server-coordinated rotation in Track A with fully on-chain blind swaps. If Option D proves too complex or expensive, Track A remains the production architecture indefinitely.
8. Cost Summary
| Operation | On-Chain Cost | At $200/SOL |
| Merkle tree creation (one-time, DevNet free) | 5-20 SOL (MainNet) | $1,000-4,000 one-time |
| Mint shard map cNFT (per entry stored) | ~0.000005 SOL | $0.001 |
| Update cNFT (rotation, per entry) | ~0.000005 SOL | $0.001 |
| Burn cNFT (entry deleted) | ~0.000005 SOL | $0.001 |
| Read cNFT metadata (every unlock) | FREE | $0.00 |
| Blind swap instruction (Option D, per swap) | ~0.000012 SOL | $0.0024 |
Annual cost per user (Guardian tier, 100 entries, weekly rotation):
Initial mint: 100 × $0.001 = $0.10
Weekly rotations: 100 entries × 52 weeks × $0.001 = $5.20
Reads: FREE
Total: $5.30/year (< 30 cents/month)
At 1M users (100M total entries):
One-time tree: $2,000
Annual mints + rotations: ~$530,000
vs. regular Solana accounts for same: ~$56,000,000
cNFTs save $55.47M annually at scale
9. The Competitive Moat
Option A makes ShardKeep the only password manager where the operator can't locate your shards.
Option D makes ShardKeep the only password manager where nobody can locate your shards — not even during rotation.
Combined, this is a cryptographic moat that no competitor can replicate without rebuilding their entire architecture from scratch.
Supersedes proposal-onchain-shard-map-v1. References: vault-security-v3, tokenomics-v3.1, addendum-revenue-streams-v1.
Anchor program 4hfvirYMHxW4nZSuTreWRQQD45Hfc4LKmUyy3hFYcZVP confirmed unaffected.
All DevNet costs are free. MainNet Merkle tree is the only significant upfront cost ($1-4K one-time).