Build a Key Custody Application
Build a key custody application using Tenzro's MPC wallet system. Create 2-of-3 threshold wallets without seed phrases, export and import encrypted keystores protected by Argon2id, rotate keys without changing addresses, set spending limits and policies, create scoped time-limited session keys, and derive addresses for multiple chains from a single wallet.
What You'll Build
- MPC wallets with 2-of-3 threshold signing (no seed phrase)
- Encrypted keystore export/import with Argon2id KDF
- Key rotation that preserves the wallet address
- Spending limits, daily caps, and recipient whitelists
- Scoped, time-limited session keys for delegated access
- Session revocation and active session monitoring
- Multi-chain key derivation (Ethereum, Solana, Bitcoin)
Security Properties
- Threshold signing — 2-of-3 Shamir Secret Sharing over GF(256), no single point of failure
- Argon2id KDF — 64 MB memory, 3 iterations, parallelism 4, resistant to GPU cracking
- Key zeroization — sensitive key material zeroized on drop via the
zeroizecrate - Signature verification — automatic post-signing verification via
tenzro_crypto::signatures::verify() - Transaction validation — chain ID, nonce, gas bounds, address format, and data size checks
Prerequisites
[dependencies]
tenzro-sdk = "0.1"
tokio = { version = "1", features = ["full"] }
chrono = "0.4"
tracing-subscriber = "0.3"Step 1: Create MPC Wallets
MPC wallets use 2-of-3 threshold signing. No seed phrase is needed — key shares are auto-provisioned and distributed across key holders:
use tenzro_sdk::{TenzroClient, config::SdkConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = SdkConfig::testnet();
let client = TenzroClient::connect(config).await?;
// Create a 2-of-3 MPC threshold wallet
// No seed phrase needed -- shares are auto-provisioned
let wallet = client.wallet().create_wallet().await?;
println!("MPC Wallet Created:");
println!(" Address: {}", wallet.address);
println!(" Threshold: 2-of-3");
println!(" Key type: Ed25519");
println!(" Shares: 3 (distributed across key holders)");MPC Wallet Created:
Address: 0x7f3a9c1e4b2d8f3a5e6c7d8e9f0a1b2c3d4e5f6a
Threshold: 2-of-3
Key type: Ed25519
Shares: 3 (distributed across key holders)Step 2: Export and Import Encrypted Keystores
Keystores are encrypted with AES-256-GCM using a key derived from your passphrase via Argon2id. The Argon2id parameters (64 MB memory, 3 iterations) make brute-force attacks impractical:
// Export encrypted keystore
// Uses Argon2id KDF: 64MB memory, 3 iterations, parallelism 4
let keystore = client.wallet().export_keystore(
&wallet.address,
"my-strong-passphrase-here",
).await?;
println!("Keystore exported:");
println!(" Format: Encrypted JSON");
println!(" KDF: Argon2id");
println!(" Memory: 64 MB");
println!(" Iterations: 3");
println!(" Size: {} bytes", keystore.len());
// Save to file
std::fs::write("wallet-backup.json", &keystore)?;
println!(" Saved to: wallet-backup.json");// Import wallet from encrypted keystore
let imported = client.wallet().import_keystore(
&std::fs::read_to_string("wallet-backup.json")?,
"my-strong-passphrase-here",
).await?;
println!("Wallet imported: {}", imported.address);
assert_eq!(imported.address, wallet.address);CLI equivalent:
# CLI keystore operations
# Create wallet
tenzro wallet create
# Export keystore (prompts for passphrase)
tenzro wallet export --address 0x7f3a...5f6a --output wallet-backup.json
# Import keystore
tenzro wallet import --keystore wallet-backup.json
# List wallets
tenzro wallet list
# Output:
# Wallets:
# 0x7f3a...5f6a Ed25519 2-of-3 Balance: 100.00 TNZOStep 3: Key Rotation
Key rotation generates fresh Shamir shares without changing the wallet address. If a share is compromised, rotation invalidates all old shares:
// Key rotation: generate new shares without changing the address
// This is critical for custody apps -- if a share is compromised,
// rotation invalidates the old shares and creates new ones
let rotation_result = client.wallet().rotate_keys(&wallet.address).await?;
println!("Key Rotation Complete:");
println!(" Address: {} (unchanged)", rotation_result.address);
println!(" Old shares: invalidated");
println!(" New shares: 3 fresh shares generated");
println!(" Threshold: still 2-of-3");
// The address stays the same because rotation generates new
// Shamir Secret Sharing shares over GF(256) while preserving
// the underlying key. All old shares are immediately invalidated.How rotation works. Shamir Secret Sharing over GF(256) allows generating new polynomial shares that reconstruct the same secret. The old shares become useless because the polynomial coefficients change while the constant term (the secret, which derives the address) stays the same. This is a standard MPC key refresh operation.
Step 4: Set Spending Limits and Policies
Policies are enforced at the wallet level before transaction signing. A transaction that violates any policy is rejected before it reaches the network:
// Set spending limits and policies
// These are enforced at the wallet level before transaction signing
let policy = client.wallet().set_policy(
&wallet.address,
WalletPolicy {
// Per-transaction limit
max_transaction: 10_000, // 10,000 TNZO max per tx
// Daily spending limit
daily_limit: 50_000, // 50,000 TNZO per day
// Whitelist of allowed recipient addresses
allowed_recipients: vec![
"0x1234...5678".to_string(), // treasury
"0xabcd...ef01".to_string(), // operations
],
// Require additional approval above threshold
approval_threshold: 5_000, // require 2nd approval above 5K
// Time-based restrictions
time_restrictions: Some(TimeRestrictions {
allowed_hours: (9, 21), // 9 AM to 9 PM UTC only
allowed_days: vec![1, 2, 3, 4, 5], // weekdays only
}),
},
).await?;
println!("Policy set:");
println!(" Max tx: 10,000 TNZO");
println!(" Daily limit: 50,000 TNZO");
println!(" Whitelist: 2 addresses");
println!(" Approval above: 5,000 TNZO");
println!(" Time window: Mon-Fri 09:00-21:00 UTC");Step 5: Create Session Keys
Session keys are temporary keys with scoped permissions. They are ideal for delegating limited wallet access to agents, trading bots, or services without exposing the master key shares:
// Create a session key (scoped, time-limited)
// Session keys are temporary keys that can only perform specific actions
// within defined constraints. Perfect for delegating limited access
// to agents or services.
let session = client.wallet().create_session_key(
&wallet.address,
SessionKeyConfig {
// Unique session identifier
session_id: "agent-trading-session".to_string(),
// What this key can do
allowed_operations: vec![
"transfer".to_string(),
"bridge".to_string(),
],
// Maximum value per transaction
max_value_per_tx: 1_000, // 1,000 TNZO per tx
// Total budget for the session
total_budget: 10_000, // 10,000 TNZO total
// Session duration
expires_at: chrono::Utc::now() + chrono::Duration::hours(4),
// Restrict to specific recipients
allowed_recipients: Some(vec![
"0x1234...5678".to_string(),
]),
// Restrict to specific chains
allowed_chains: Some(vec![
"tenzro".to_string(),
"ethereum".to_string(),
]),
},
).await?;
println!("Session Key Created:");
println!(" Session ID: {}", session.session_id);
println!(" Public Key: {}", session.public_key);
println!(" Operations: {:?}", session.allowed_operations);
println!(" Max per tx: {} TNZO", session.max_value_per_tx);
println!(" Budget: {} TNZO", session.total_budget);
println!(" Expires: {}", session.expires_at);Session Key Created:
Session ID: agent-trading-session
Public Key: 0x8a3f...c4d1
Operations: ["transfer", "bridge"]
Max per tx: 1000 TNZO
Budget: 10000 TNZO
Expires: 2026-04-12T18:00:00ZUsing a Session Key
// Use a session key for a transaction
// The session key signs the transaction, and the wallet validates
// against the session constraints before broadcasting
let tx = client.wallet().send_with_session(
&session.session_id,
"0x1234...5678", // recipient (must be in whitelist)
500, // 500 TNZO (under 1K per-tx limit)
).await?;
println!("Transaction sent via session key:");
println!(" Tx hash: {}", tx.hash);
println!(" Session: {}", session.session_id);
println!(" Remaining: {} TNZO budget", session.total_budget - 500);
// Attempt to exceed session limits
let err = client.wallet().send_with_session(
&session.session_id,
"0x1234...5678",
5_000, // 5,000 TNZO -- exceeds 1K per-tx limit
).await;
match err {
Err(e) => println!("Blocked: {}", e), // "exceeds max_value_per_tx"
Ok(_) => unreachable!(),
}Step 6: Revoke Sessions
// Revoke a session key
// Immediately invalidates the session -- no further transactions allowed
client.wallet().revoke_session(&wallet.address, &session.session_id).await?;
println!("Session revoked: {}", session.session_id);
// List active sessions
let sessions = client.wallet().list_sessions(&wallet.address).await?;
for s in &sessions {
println!(" {} -- ops: {:?}, expires: {}, budget: {} TNZO",
s.session_id, s.allowed_operations, s.expires_at, s.remaining_budget);
}Step 7: Multi-Chain Key Derivation
Derive addresses for multiple chains from the same MPC wallet. Each chain uses its native curve (secp256k1 for Ethereum/Bitcoin, Ed25519 for Solana/Tenzro):
// Multi-chain key derivation
// Derive addresses for different chains from the same MPC wallet
let eth_address = client.wallet().derive_address(
&wallet.address,
"ethereum",
"secp256k1", // Ethereum uses secp256k1
).await?;
let sol_address = client.wallet().derive_address(
&wallet.address,
"solana",
"ed25519", // Solana uses Ed25519
).await?;
let btc_address = client.wallet().derive_address(
&wallet.address,
"bitcoin",
"secp256k1",
).await?;
println!("Multi-Chain Addresses:");
println!(" Tenzro: {}", wallet.address);
println!(" Ethereum: {}", eth_address);
println!(" Solana: {}", sol_address);
println!(" Bitcoin: {}", btc_address);Multi-Chain Addresses:
Tenzro: 0x7f3a9c1e4b2d8f3a5e6c7d8e9f0a1b2c3d4e5f6a
Ethereum: 0x4b2d8f3a5e6c7d8e9f0a1b2c3d4e5f6a7f3a9c1e
Solana: 7Kj4mR9fBn2xWqYz5Lp8sT3vU6hF1cD0eA9gH2jM4kN
Bitcoin: bc1q7f3a9c1e4b2d8f3a5e6c7d8e9f0a1b2c3dFull Example
Run with:
cargo run --example custody_appView full source
use tenzro_sdk::{TenzroClient, config::SdkConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let client = TenzroClient::connect(SdkConfig::testnet()).await?;
// 1. Create MPC wallet
let wallet = client.wallet().create_wallet().await?;
println!("Wallet: {}", wallet.address);
// 2. Export encrypted keystore
let keystore = client.wallet().export_keystore(
&wallet.address, "passphrase",
).await?;
std::fs::write("backup.json", &keystore)?;
// 3. Set spending policy
client.wallet().set_policy(&wallet.address, WalletPolicy {
max_transaction: 10_000,
daily_limit: 50_000,
allowed_recipients: vec![],
approval_threshold: 5_000,
time_restrictions: None,
}).await?;
// 4. Create session key
let session = client.wallet().create_session_key(
&wallet.address,
SessionKeyConfig {
session_id: "demo-session".into(),
allowed_operations: vec!["transfer".into()],
max_value_per_tx: 1_000,
total_budget: 10_000,
expires_at: chrono::Utc::now() + chrono::Duration::hours(4),
allowed_recipients: None,
allowed_chains: None,
},
).await?;
// 5. Use session key
let tx = client.wallet().send_with_session(
&session.session_id, "0x1234...5678", 500,
).await?;
println!("Tx: {}", tx.hash);
// 6. Revoke session
client.wallet().revoke_session(&wallet.address, &session.session_id).await?;
// 7. Derive multi-chain addresses
let eth = client.wallet().derive_address(&wallet.address, "ethereum", "secp256k1").await?;
println!("Ethereum: {}", eth);
// 8. Rotate keys
client.wallet().rotate_keys(&wallet.address).await?;
println!("Keys rotated (address unchanged)");
Ok(())
}What You Learned
- MPC wallets — 2-of-3 threshold signing without seed phrases
- Encrypted keystores — Argon2id-protected export and import
- Key rotation — refreshing shares without changing addresses
- Spending policies — per-transaction limits, daily caps, recipient whitelists, time windows
- Session keys — scoped, time-limited, budget-capped delegated access
- Session revocation — immediate invalidation of compromised sessions
- Multi-chain derivation — one wallet, addresses on Tenzro, Ethereum, Solana, Bitcoin
Next Steps
- See the TEE Confidential Computing tutorial for TEE-backed key management
- See the Create Agentic Wallet tutorial for agent-controlled wallets
- Read the Network Plugin Agent tutorial for session keys in agent delegation