Threshold Cryptography
LP-7341
Review@luxfi/threshold TypeScript SDK
TypeScript SDK for interacting with T-Chain threshold signature services
Abstract
This LP specifies @luxfi/threshold, the TypeScript SDK for interacting with T-Chain (ThresholdVM) threshold signature services. The SDK provides a high-level client for key generation, signing, resharing, and key management operations, abstracting the JSON-RPC protocol into a clean async/await interface.
Motivation
A TypeScript SDK enables:
- Web Integration: Easy integration with web applications and wallets
- Bridge Frontend: UI for bridge operations requiring threshold signatures
- Developer Experience: Type-safe API with IntelliSense support
- Cross-Platform: Works in Node.js, browsers, and React Native
- Testing: Simplifies testing of T-Chain integrations
Specification
Package Location
@luxfi/threshold (bridge/pkg/threshold/)
├── src/
│ ├── index.ts # Package exports
│ ├── client.ts # ThresholdClient class
│ └── types.ts # TypeScript types
├── test/
│ └── client.test.ts # Test suite
├── package.json
└── tsconfig.json
Installation
npm install @luxfi/threshold
# or
pnpm add @luxfi/threshold
ThresholdClient
import { ThresholdClient } from '@luxfi/threshold'
const client = new ThresholdClient({
endpoint: 'http://localhost:9650/ext/bc/T'
})
Client Options
interface ThresholdClientOptions {
/** T-Chain RPC endpoint */
endpoint: string
/** Request timeout in milliseconds (default: 30000) */
timeout?: number
/** Retry configuration */
retry?: {
maxAttempts?: number // default: 3
baseDelay?: number // default: 1000ms
maxDelay?: number // default: 10000ms
}
}
Core Methods
Key Generation
// Request key generation
const keygen = await client.keygen({
keyId: 'eth-custody-key',
threshold: 3,
totalParties: 5,
protocol: 'lss', // 'lss' | 'cggmp21' | 'frost'
curve: 'secp256k1' // 'secp256k1' | 'ed25519'
})
// Returns session info
console.log(keygen.sessionId) // 'keygen-abc123'
// Wait for completion
const result = await client.waitForKeygen(keygen.sessionId)
console.log(result.publicKey) // '0x04...'
Chain-Based Keygen (Auto Protocol Selection)
// Auto-selects best protocol for target chain
const ethKey = await client.keygen({
keyId: 'eth-key',
chain: 'ethereum' // Uses LSS protocol, secp256k1
})
const solKey = await client.keygen({
keyId: 'sol-key',
chain: 'solana' // Uses FROST protocol, ed25519
})
const btcKey = await client.keygen({
keyId: 'btc-taproot',
chain: 'bitcoin',
options: { taproot: true } // BIP-340 x-only keys
})
Signing
// Request signature
const signResult = await client.sign({
keyId: 'eth-custody-key',
messageHash: '0x1234567890abcdef...',
messageType: 'eth_sign'
})
// Wait for signature
const sig = await client.waitForSignature(signResult.sessionId)
console.log(sig.r) // BigInt
console.log(sig.s) // BigInt
console.log(sig.v) // 27 or 28
Combined Sign and Wait
const signature = await client.signAndWait({
keyId: 'eth-custody-key',
messageHash: '0x...',
messageType: 'eth_sign'
})
Key Information
const keyInfo = await client.getKey('eth-custody-key')
console.log(keyInfo.publicKey)
console.log(keyInfo.threshold)
console.log(keyInfo.totalParties)
console.log(keyInfo.generation)
console.log(keyInfo.protocol)
Resharing
// Reshare to new parties
const reshare = await client.reshare({
keyId: 'eth-custody-key',
newParties: ['party6', 'party7', 'party8'],
newThreshold: 2
})
await client.waitForReshare(reshare.sessionId)
Key Refresh
// Refresh shares without changing public key
await client.refresh('eth-custody-key')
Type Definitions
// Protocol types
type Protocol = 'lss' | 'cggmp21' | 'frost' | 'bls' | 'ringtail'
type Chain = 'ethereum' | 'bitcoin' | 'solana' | 'lux' | 'polygon' | 'arbitrum'
type Curve = 'secp256k1' | 'ed25519' | 'bls12-381'
// Request types
interface KeygenRequest {
keyId: string
threshold?: number
totalParties?: number
protocol?: Protocol
curve?: Curve
chain?: Chain
options?: {
taproot?: boolean
timeout?: number
}
}
interface SignRequest {
keyId: string
messageHash: string
messageType?: 'raw' | 'eth_sign' | 'typed_data'
signers?: string[]
}
// Response types
interface KeygenResponse {
sessionId: string
status: 'pending' | 'running' | 'completed' | 'failed'
}
interface SignatureResponse {
r: bigint
s: bigint
v?: number
signature: string // Hex-encoded full signature
}
interface KeyInfo {
keyId: string
publicKey: string
protocol: Protocol
curve: Curve
threshold: number
totalParties: number
generation: number
createdAt: number
lastRefresh?: number
}
// Session types
interface SessionInfo {
sessionId: string
type: 'keygen' | 'sign' | 'reshare' | 'refresh'
status: 'pending' | 'running' | 'completed' | 'failed'
progress?: number
error?: string
}
Error Handling
import { ThresholdClient, ThresholdError } from '@luxfi/threshold'
try {
await client.sign({ keyId: 'unknown', messageHash: '0x...' })
} catch (error) {
if (error instanceof ThresholdError) {
console.log(error.code) // 'KEY_NOT_FOUND'
console.log(error.message) // 'Key "unknown" not found'
}
}
Error Codes:
KEY_NOT_FOUND: Requested key does not existSESSION_NOT_FOUND: Session ID not foundINSUFFICIENT_SIGNERS: Not enough signers availableTIMEOUT: Operation timed outNETWORK_ERROR: RPC connection failedINVALID_MESSAGE: Invalid message formatSIGNATURE_FAILED: Signing protocol failed
Advanced Usage
Listing Keys
const keys = await client.listKeys({
protocol: 'lss',
limit: 100
})
Session Monitoring
// List active sessions
const sessions = await client.listSessions({
type: 'sign',
status: 'running'
})
// Get session details
const session = await client.getSession('sign-xyz789')
Health Check
const health = await client.health()
console.log(health.status) // 'healthy'
console.log(health.protocols) // ['lss', 'cggmp21', 'frost']
console.log(health.signerCount) // 5
Network Statistics
const stats = await client.getNetworkStats()
console.log(stats.totalKeys)
console.log(stats.totalSignatures)
console.log(stats.averageSignTime)
Usage Examples
Bridge Integration
import { ThresholdClient } from '@luxfi/threshold'
import { ethers } from 'ethers'
async function bridgeWithdraw(
client: ThresholdClient,
withdrawalTx: ethers.TransactionRequest
) {
// Hash the transaction
const txHash = ethers.keccak256(
ethers.Transaction.from(withdrawalTx).serialized
)
// Get threshold signature
const sig = await client.signAndWait({
keyId: 'bridge-eth-custody',
messageHash: txHash,
messageType: 'eth_sign'
})
// Add signature to transaction
withdrawalTx.signature = {
r: '0x' + sig.r.toString(16).padStart(64, '0'),
s: '0x' + sig.s.toString(16).padStart(64, '0'),
v: sig.v
}
// Broadcast
const provider = new ethers.JsonRpcProvider(ETH_RPC)
return provider.broadcastTransaction(
ethers.Transaction.from(withdrawalTx).serialized
)
}
Solana Integration
import { ThresholdClient } from '@luxfi/threshold'
async function signSolanaTransaction(
client: ThresholdClient,
message: Uint8Array
) {
const sig = await client.signAndWait({
keyId: 'bridge-sol-custody',
messageHash: Buffer.from(message).toString('hex'),
messageType: 'raw'
})
// Return Ed25519 signature
return Buffer.from(sig.signature, 'hex')
}
Testing
cd bridge/pkg/threshold
pnpm test
# Output:
# ✓ ThresholdClient initializes with endpoint
# ✓ keygen returns session info
# ✓ sign returns signature
# ✓ waitForKeygen polls until complete
# ✓ signAndWait combines operations
# ✓ handles network errors with retry
# ✓ validates input parameters
# ✓ supports chain-based protocol selection
Rationale
TypeScript-First Design
Choosing TypeScript provides:
- Type Safety: Compile-time errors for API misuse
- IntelliSense: Autocomplete for all methods and options
- Documentation: Types serve as inline documentation
- Ecosystem: Wide adoption in web3 and DeFi development
Async/Await API
The async/await pattern provides:
- Simplicity: Clean, readable code without callback hell
- Error Handling: Native try/catch for error management
- Composability: Easy to chain operations and use with other async code
Chain-Based Protocol Selection
Auto-selecting protocols based on target chain:
- Developer Experience: No need to know which protocol to use
- Correctness: Ensures appropriate curve and signature format
- Flexibility: Override available when explicit control needed
Backwards Compatibility
T-Chain RPC Compatibility
The SDK is compatible with T-Chain RPC versions:
- v1.0.x: Full support for all methods
- Future versions: Protocol negotiation via health check
ethers.js Compatibility
Works with ethers.js v6:
- Uses standard types (BigInt, hex strings)
- Compatible with ethers Transaction and Wallet types
- Works alongside ethers providers
Browser Compatibility
Supports modern browsers:
- Chrome 80+, Firefox 75+, Safari 13+
- React Native with proper polyfills
- Node.js 16+
Migration from Direct RPC
Migrating from direct RPC calls:
// Before (direct RPC)
const result = await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method: 'threshold.keygen',
params: [{ keyId: 'key1', threshold: 3, parties: 5 }]
})
})
// After (SDK)
const client = new ThresholdClient({ endpoint })
const result = await client.keygen({ keyId: 'key1', threshold: 3, totalParties: 5 })
Test Cases
Client Tests
describe('ThresholdClient', () => {
it('initializes with valid endpoint', () => {
const client = new ThresholdClient({ endpoint: 'http://localhost:9650/ext/bc/T' });
expect(client).toBeDefined();
});
it('keygen returns session info', async () => {
const result = await client.keygen({
keyId: 'test-key',
threshold: 2,
totalParties: 3,
protocol: 'lss'
});
expect(result.sessionId).toBeDefined();
expect(result.status).toBe('pending');
});
it('waitForKeygen polls until complete', async () => {
const keygen = await client.keygen({ keyId: 'poll-test', threshold: 2, totalParties: 3 });
const result = await client.waitForKeygen(keygen.sessionId);
expect(result.publicKey).toMatch(/^0x04/);
expect(result.publicKey.length).toBe(130);
});
it('sign returns valid signature', async () => {
const sig = await client.signAndWait({
keyId: 'test-key',
messageHash: '0x' + '00'.repeat(32),
messageType: 'raw'
});
expect(sig.r).toBeInstanceOf(BigInt);
expect(sig.s).toBeInstanceOf(BigInt);
expect(sig.signature).toMatch(/^0x[a-f0-9]+$/);
});
it('handles network errors with retry', async () => {
const badClient = new ThresholdClient({
endpoint: 'http://invalid:9999',
retry: { maxAttempts: 2, baseDelay: 100 }
});
await expect(badClient.health()).rejects.toThrow(ThresholdError);
});
});
Protocol Selection Tests
describe('Chain-based protocol selection', () => {
it('selects LSS for Ethereum', async () => {
const keygen = await client.keygen({ keyId: 'eth-key', chain: 'ethereum' });
const key = await client.getKey('eth-key');
expect(key.protocol).toBe('lss');
expect(key.curve).toBe('secp256k1');
});
it('selects FROST for Bitcoin Taproot', async () => {
const keygen = await client.keygen({
keyId: 'btc-key',
chain: 'bitcoin',
options: { taproot: true }
});
const key = await client.getKey('btc-key');
expect(key.protocol).toBe('frost');
expect(key.curve).toBe('secp256k1');
});
it('selects FROST with Ed25519 for Solana', async () => {
const keygen = await client.keygen({ keyId: 'sol-key', chain: 'solana' });
const key = await client.getKey('sol-key');
expect(key.protocol).toBe('frost');
expect(key.curve).toBe('ed25519');
});
});
Error Handling Tests
describe('Error handling', () => {
it('throws KEY_NOT_FOUND for unknown key', async () => {
try {
await client.getKey('nonexistent');
fail('Should have thrown');
} catch (error) {
expect(error).toBeInstanceOf(ThresholdError);
expect((error as ThresholdError).code).toBe('KEY_NOT_FOUND');
}
});
it('throws TIMEOUT when operation exceeds limit', async () => {
const slowClient = new ThresholdClient({
endpoint,
timeout: 1 // 1ms timeout
});
await expect(slowClient.health()).rejects.toMatchObject({
code: 'TIMEOUT'
});
});
});
Related LPs
- LP-7330: T-Chain ThresholdVM Specification (RPC API)
- LP-7340: Threshold Cryptography Library (Go implementation)
- LP-7014: CMP/CGGMP21 Protocol
- LP-7103: LSS Protocol
- LP-7104: FROST Protocol
Security Considerations
- No Private Data: SDK never handles private key shares
- HTTPS Required: Production use must use HTTPS endpoints
- Message Validation: Validate message hashes before signing
- Access Control: Implement authorization before signing requests
- Rate Limiting: Implement client-side rate limiting
Copyright
Copyright and related rights waived via CC0.