lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
1,862 lines (1,497 loc) • 52.4 kB
Markdown
# SwapSig Implementation Plan
**Version**: 2.1
**Date**: November 28, 2025
**Status**: Complete Re-Implementation Required (Updated with v2 Security Analysis)
---
## Overview
This document outlines the complete re-implementation plan for SwapSig following the MuSig2 P2P architecture rearchitecture. The previous implementation is no longer valid and must be rebuilt from scratch, incorporating fixes for the security/privacy gaps identified in:
- `SWAPSIG_SECURITY_PRIVACY_GAPS.md` (v1 analysis)
- `SWAPSIG_SECURITY_PRIVACY_GAPS_V2.md` (v2 comprehensive analysis)
### Key Updates in v2.1
Based on the comprehensive v2 security analysis, this plan now includes:
1. **Intersection Attack Prevention** - Wallet-level output isolation
2. **Fee Rate Obfuscation** - Randomized fees to prevent fingerprinting
3. **Amount Variance** - Slight randomization to break amount correlation
4. **Timelock Refunds** - Lotus OP_CHECKSEQUENCEVERIFY for trustless refunds
5. **Graph Obfuscation** - Decoy transactions and pool chaining support
6. **Privacy Modes** - Tiered privacy (Standard/Enhanced/Maximum)
---
## Architecture Overview
### Component Hierarchy
```
lib/p2p/swapsig/
├── index.ts # Public exports
├── types.ts # Type definitions
├── coordinator.ts # Main SwapSigCoordinator class
├── pool.ts # Pool management
├── protocol.ts # Protocol state machine
├── discovery.ts # DHT discovery (privacy-preserving)
├── commitment.ts # Secure destination commitments
├── burn.ts # Stealth burn mechanism
├── timing.ts # Timing obfuscation
├── validation.ts # Input/output validation
├── privacy.ts # Privacy utilities
├── security.ts # Security mechanisms
├── errors.ts # Error types
├── wallet.ts # Wallet integration (intersection prevention) [NEW]
├── refund.ts # Timelock refund scripts (Lotus CSV) [NEW]
├── obfuscation.ts # Fee/amount obfuscation [NEW]
├── decoy.ts # Decoy transaction generation [NEW]
└── README.md # Module documentation
```
### Integration with MuSig2 P2P
```
┌─────────────────────────────────────────────────────────────┐
│ SwapSigCoordinator │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Pool Management | Protocol FSM | Privacy Layer │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ Uses
▼
┌─────────────────────────────────────────────────────────────┐
│ MuSig2P2PCoordinator │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Discovery Layer | Session Management | Signing │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ Uses
▼
┌─────────────────────────────────────────────────────────────┐
│ P2PCoordinator │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ libp2p | DHT | GossipSub | Direct Messaging │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## Implementation Phases
### Phase 1: Core Types & Infrastructure (Week 1)
**Files to Create**:
- `types.ts` - All type definitions
- `errors.ts` - Error classes
- `index.ts` - Public exports
**Key Types**:
```typescript
// types.ts
// ============================================================================
// Pool Types
// ============================================================================
export enum SwapPhase {
DISCOVERY = 'discovery',
REGISTRATION = 'registration',
COMMITMENT = 'commitment',
SETUP = 'setup',
SETUP_CONFIRM = 'setup-confirm',
REVEAL = 'reveal',
SETTLEMENT = 'settlement',
SETTLEMENT_CONFIRM = 'settlement-confirm',
COMPLETE = 'complete',
ABORTED = 'aborted',
}
export interface SwapPool {
poolId: string
denomination: number
minParticipants: number
maxParticipants: number
feeRate: number
// Privacy configuration
privacyConfig: PoolPrivacyConfig
// Burn configuration
burnConfig: BurnConfig
// Group size strategy (computed)
groupSizeStrategy?: GroupSizeStrategy
// Participants
participants: SwapParticipant[]
participantMap: Map<string, SwapParticipant>
// Outputs
outputGroups: OutputGroup[]
settlementMapping: Map<number, SettlementInfo>
// State
phase: SwapPhase
createdAt: number
startedAt?: number
completedAt?: number
aborted: boolean
abortReason?: string
// Timeouts
setupTimeout: number
settlementTimeout: number
}
export interface SwapParticipant {
peerId: string
participantIndex: number
publicKey: PublicKey
// Input
input: SwapInput
ownershipProof: Buffer
// Secure destination commitment (NEW)
destinationCommitment: SecureCommitment
revealedDestination?: Address
// Transaction status
setupTxId?: string
setupConfirmed: boolean
burnVerified: boolean
joinedAt: number
}
// ============================================================================
// Privacy Types (NEW - Addresses Security Gaps)
// ============================================================================
export interface PoolPrivacyConfig {
// Network privacy
requireAnonymousTransport: boolean
allowedTransports: ('tor' | 'i2p' | 'direct')[]
// Timing obfuscation
enableTimingObfuscation: boolean
setupBroadcastWindowMs: number
settlementBroadcastWindowMs: number
// Coordinator blindness
enableBlindCoordinator: boolean
// On-chain privacy
enableStealthBurn: boolean
// DHT privacy
enableAnonymousPoolAnnouncement: boolean
}
export interface SecureCommitment {
// Pedersen commitment: C = g^m * h^r
commitment: Buffer // 33 bytes (compressed point)
// Per-participant encrypted destinations
encryptedDestinations: Map<string, Buffer> // peerId -> encrypted
// Reveal data (only set after reveal)
revealData?: {
destination: Address
blindingFactor: Buffer
}
}
export interface StealthBurnConfig {
// Burn method
method: 'fee-overpay' | 'unspendable-p2pkh' | 'generic-opreturn'
// Verification
offChainVerification: boolean
burnCommitmentRequired: boolean
}
// ============================================================================
// Group Types
// ============================================================================
export interface OutputGroup {
groupIndex: number
participantIndices: number[]
signers: PublicKey[]
// MuSig2 aggregated key
aggregatedKey: PublicKey
taprootAddress: Address
// Amount per output
amount: number
// Settlement info
receiverIndex: number
receiverAddress?: Address
// Transaction status
setupTxIds: string[]
settlementTxId?: string
settlementConfirmed: boolean
// MuSig2 session
signingRequestId?: string
sessionId?: string
}
export interface GroupSizeStrategy {
groupSize: number // 2, 3, 5, or 10
groupCount: number
anonymityPerGroup: number
reasoning: string
recommendedRounds: number
}
```
**Deliverables**:
- [ ] Complete type definitions
- [ ] Error class hierarchy
- [ ] Public export structure
---
### Phase 2: Secure Commitment Scheme (Week 2)
**Files to Create**:
- `commitment.ts` - Pedersen commitments and secure reveal
**Implementation**:
```typescript
// commitment.ts
import { Point, BN } from '../../bitcore/crypto/index.js'
import { Hash } from '../../bitcore/crypto/hash.js'
import { PublicKey } from '../../bitcore/publickey.js'
import { PrivateKey } from '../../bitcore/privatekey.js'
import type { Address } from '../../bitcore/address.js'
/**
* Secure Destination Commitment
*
* Uses Pedersen commitments for information-theoretic hiding:
* C = g^m * h^r
*
* Where:
* - g, h are generator points (h = Hash-to-curve of g)
* - m is the message (destination address hash)
* - r is the blinding factor (random)
*/
export class SecureDestinationCommitment {
// Generator points
private static readonly G: Point = Point.getG()
private static readonly H: Point = this._deriveH()
private static _deriveH(): Point {
// H = Hash-to-curve(G) - nothing-up-my-sleeve derivation
const gBytes = this.G.toBuffer()
const hHash = Hash.sha256(
Buffer.concat([Buffer.from('SwapSig_H_Generator'), gBytes]),
)
// Use hash as x-coordinate, find valid point
return Point.fromX(hHash)
}
/**
* Create a Pedersen commitment to a destination address
*/
static commit(destination: Address): {
commitment: Buffer
blindingFactor: Buffer
} {
// Generate random blinding factor
const blindingFactor = PrivateKey.fromRandom().bn.toBuffer()
// Hash destination to scalar
const destHash = Hash.sha256(destination.toBuffer())
const m = BN.fromBuffer(destHash)
const r = BN.fromBuffer(blindingFactor)
// C = g^m * h^r
const gm = this.G.mul(m)
const hr = this.H.mul(r)
const C = gm.add(hr)
return {
commitment: C.toBuffer(),
blindingFactor,
}
}
/**
* Verify a commitment opening
*/
static verify(
commitment: Buffer,
destination: Address,
blindingFactor: Buffer,
): boolean {
const destHash = Hash.sha256(destination.toBuffer())
const m = BN.fromBuffer(destHash)
const r = BN.fromBuffer(blindingFactor)
// Recompute C = g^m * h^r
const gm = this.G.mul(m)
const hr = this.H.mul(r)
const expectedC = gm.add(hr)
// Compare
return expectedC.toBuffer().equals(commitment)
}
/**
* Encrypt destination for specific recipient
* Uses ECIES-like encryption
*/
static encryptForRecipient(
destination: Address,
recipientPubKey: PublicKey,
senderPrivKey: PrivateKey,
): Buffer {
// ECDH shared secret
const sharedPoint = recipientPubKey.point.mul(senderPrivKey.bn)
const sharedSecret = Hash.sha256(sharedPoint.toBuffer())
// Encrypt destination with shared secret (AES-256-GCM)
const destBytes = destination.toBuffer()
return this._aesEncrypt(destBytes, sharedSecret)
}
/**
* Decrypt destination from sender
*/
static decryptFromSender(
encrypted: Buffer,
senderPubKey: PublicKey,
recipientPrivKey: PrivateKey,
): Address {
// ECDH shared secret
const sharedPoint = senderPubKey.point.mul(recipientPrivKey.bn)
const sharedSecret = Hash.sha256(sharedPoint.toBuffer())
// Decrypt
const destBytes = this._aesDecrypt(encrypted, sharedSecret)
return Address.fromBuffer(destBytes)
}
private static _aesEncrypt(data: Buffer, key: Buffer): Buffer {
// Implementation using crypto module
// ... AES-256-GCM encryption
}
private static _aesDecrypt(data: Buffer, key: Buffer): Buffer {
// Implementation using crypto module
// ... AES-256-GCM decryption
}
}
```
**Deliverables**:
- [ ] Pedersen commitment implementation
- [ ] ECIES encryption for per-recipient destinations
- [ ] Commitment verification
- [ ] Unit tests for commitment scheme
---
### Phase 3: Stealth Burn Mechanism (Week 2-3)
**Files to Create**:
- `burn.ts` - Privacy-preserving burn mechanism
**Implementation**:
```typescript
// burn.ts
import { Transaction, Output, Script, Opcode } from '../../bitcore/index.js'
import { Hash } from '../../bitcore/crypto/hash.js'
import type { BurnConfig, StealthBurnConfig } from './types.js'
/**
* Stealth Burn Mechanism
*
* Provides Sybil defense without on-chain protocol fingerprinting.
*
* Methods:
* 1. Fee Overpay: Burn via excessive transaction fee (miners receive)
* 2. Unspendable P2PKH: Burn to known-unspendable address
* 3. Generic OP_RETURN: Random data, no identifier
*/
export class StealthBurnMechanism {
private config: StealthBurnConfig
constructor(config: StealthBurnConfig) {
this.config = config
}
/**
* Calculate required burn amount
*/
calculateBurnAmount(
denomination: number,
burnPercentage: number,
poolSize: number,
): number {
// Base burn
let burn = Math.floor(denomination * burnPercentage)
// Scale with pool size (larger pools = slightly higher burn)
const poolMultiplier = 1 + Math.log10(poolSize) * 0.1
burn = Math.floor(burn * poolMultiplier)
// Apply bounds
const minBurn = 1000 // 0.001 XPI
const maxBurn = 100000 // 0.1 XPI
return Math.max(minBurn, Math.min(burn, maxBurn))
}
/**
* Create burn output (stealth mode)
*/
createBurnOutput(
burnAmount: number,
poolId: string,
): { output?: Output; feeAddition?: number; commitment: Buffer } {
// Create off-chain commitment (for verification)
const commitment = Hash.sha256(
Buffer.concat([
Buffer.from(poolId, 'hex'),
Buffer.from(burnAmount.toString()),
Buffer.from(Date.now().toString()),
]),
)
switch (this.config.method) {
case 'fee-overpay':
// No output, just add to fee
return {
feeAddition: burnAmount,
commitment,
}
case 'unspendable-p2pkh':
// Burn to provably unspendable address
// Hash160 of empty = known unspendable
const unspendableHash = Hash.sha256ripemd160(Buffer.alloc(0))
const script = Script.buildPublicKeyHashOut(unspendableHash)
return {
output: new Output({ satoshis: burnAmount, script }),
commitment,
}
case 'generic-opreturn':
// Random data, no identifier
const randomData = Hash.sha256(
Buffer.concat([commitment, Buffer.from(Math.random().toString())]),
)
const opReturnScript = new Script()
.add(Opcode.OP_RETURN)
.add(randomData.slice(0, 20)) // 20 random bytes
return {
output: new Output({ satoshis: burnAmount, script: opReturnScript }),
commitment,
}
}
}
/**
* Verify burn in transaction (off-chain)
*
* Participants exchange signed burn commitments
* No on-chain verification needed
*/
async verifyBurnCommitment(
setupTxId: string,
expectedBurnAmount: number,
burnCommitment: Buffer,
participantSignature: Buffer,
participantPubKey: PublicKey,
): Promise<boolean> {
// Verify signature over commitment
const message = Buffer.concat([
Buffer.from(setupTxId, 'hex'),
burnCommitment,
])
if (!Schnorr.verify(message, participantSignature, participantPubKey)) {
return false
}
// Verify transaction exists and has sufficient burn
// (fee overpay or unspendable output)
const tx = await this._fetchTransaction(setupTxId)
switch (this.config.method) {
case 'fee-overpay':
const inputValue = await this._getInputValue(tx)
const outputValue = tx.outputs.reduce((sum, o) => sum + o.satoshis, 0)
const fee = inputValue - outputValue
return fee >= expectedBurnAmount + this._getBaseFee(tx)
case 'unspendable-p2pkh':
case 'generic-opreturn':
// Find burn output
const burnOutput = tx.outputs.find(
o =>
o.satoshis >= expectedBurnAmount &&
(o.script.isDataOut() || this._isUnspendable(o.script)),
)
return burnOutput !== undefined
}
return false
}
private _isUnspendable(script: Script): boolean {
// Check if P2PKH to known unspendable hash
const unspendableHash = Hash.sha256ripemd160(Buffer.alloc(0))
// ... implementation
return false
}
}
```
**Deliverables**:
- [ ] Three burn methods implemented
- [ ] Off-chain verification
- [ ] Burn commitment signing
- [ ] Unit tests
---
### Phase 4: Privacy-Preserving Discovery (Week 3)
**Files to Create**:
- `discovery.ts` - Anonymous pool discovery
**Implementation**:
```typescript
// discovery.ts
import type { MuSig2P2PCoordinator } from '../musig2/coordinator.js'
import type { SwapPool, PoolPrivacyConfig } from './types.js'
/**
* Privacy-Preserving Pool Discovery
*
* Addresses DHT privacy leakage by:
* 1. Anonymous pool announcements (no creator identity)
* 2. Onion-routed DHT queries
* 3. Encrypted participant lists
*/
export class SwapSigDiscovery {
private coordinator: MuSig2P2PCoordinator
private privacyConfig: PoolPrivacyConfig
constructor(
coordinator: MuSig2P2PCoordinator,
privacyConfig: PoolPrivacyConfig,
) {
this.coordinator = coordinator
this.privacyConfig = privacyConfig
}
/**
* Announce pool anonymously
*/
async announcePool(pool: SwapPool): Promise<void> {
// Create anonymous announcement
const announcement: AnonymousPoolAnnouncement = {
poolId: pool.poolId,
denomination: pool.denomination,
minParticipants: pool.minParticipants,
maxParticipants: pool.maxParticipants,
currentParticipants: pool.participants.length,
// NO creator identity
// Use ring signature to prove valid creation
creatorProof: await this._createRingSignature(pool),
// Encrypted metadata
encryptedMetadata: this._encryptPoolMetadata(pool),
// Expiration
expiresAt: Date.now() + pool.setupTimeout + pool.settlementTimeout,
}
// Announce via onion routing if enabled
if (this.privacyConfig.enableAnonymousPoolAnnouncement) {
await this._announceViaOnionRoute(announcement)
} else {
await this.coordinator.announceResource(
'swapsig-pool',
pool.poolId,
announcement,
)
}
}
/**
* Discover pools with privacy
*/
async discoverPools(filters: PoolFilters): Promise<PoolAnnouncement[]> {
// Query DHT via Tor/onion if enabled
let announcements: PoolAnnouncement[]
if (this.privacyConfig.requireAnonymousTransport) {
announcements = await this._queryViaOnionRoute(filters)
} else {
announcements = await this.coordinator.discoverResources(
'swapsig-pool',
filters,
)
}
// Filter and validate
return announcements
.filter(a => this._validateAnnouncement(a))
.filter(a => this._matchesFilters(a, filters))
}
/**
* Join pool anonymously
*/
async joinPool(poolId: string, participant: SwapParticipant): Promise<void> {
// Create anonymous join message
const joinMessage: AnonymousJoinMessage = {
poolId,
// Participant info (encrypted to pool creator)
encryptedParticipant: await this._encryptParticipant(participant),
// Proof of valid input (without revealing identity)
inputProof: await this._createInputProof(participant),
// Commitment to destination
destinationCommitment: participant.destinationCommitment.commitment,
}
// Send via anonymous channel
await this._sendAnonymousJoin(joinMessage)
}
private async _createRingSignature(pool: SwapPool): Promise<Buffer> {
// Ring signature proves creator is a valid participant
// without revealing which one
// ... implementation using existing crypto
return Buffer.alloc(64)
}
private async _announceViaOnionRoute(
announcement: AnonymousPoolAnnouncement,
): Promise<void> {
// Select random intermediate nodes
const hops = await this._selectOnionHops(3)
// Encrypt in layers (onion)
let payload = Buffer.from(JSON.stringify(announcement))
for (const hop of hops.reverse()) {
payload = await this._encryptForHop(payload, hop)
}
// Send to first hop
await this.coordinator.sendTo(hops[0], {
type: 'swapsig:onion-announce',
payload,
})
}
}
```
**Deliverables**:
- [ ] Anonymous pool announcements
- [ ] Onion-routed DHT operations
- [ ] Ring signature for creator proof
- [ ] Unit tests
---
### Phase 5: Timing Obfuscation (Week 3-4)
**Files to Create**:
- `timing.ts` - Timing decorrelation
**Implementation**:
```typescript
// timing.ts
/**
* Timing Obfuscation
*
* Prevents timing correlation attacks by:
* 1. Random delays with exponential distribution
* 2. Broadcast window spreading
* 3. Decoy transaction support
*/
export class TimingObfuscation {
private config: TimingConfig
constructor(config: TimingConfig) {
this.config = config
}
/**
* Get random delay with exponential distribution
*
* Exponential distribution is memoryless, making
* timing analysis much harder than uniform distribution
*/
getRandomDelay(windowMs: number): number {
// Exponential distribution: -ln(U) * mean
// Mean = window / 3 (so most delays are within window)
const mean = windowMs / 3
const u = Math.random()
const delay = -Math.log(u) * mean
// Cap at 2x window to prevent extreme outliers
return Math.min(delay, windowMs * 2)
}
/**
* Schedule transaction broadcast with obfuscation
*/
async scheduleBroadcast(
tx: Transaction,
windowMs: number,
broadcaster: TransactionBroadcaster,
): Promise<string> {
const delay = this.getRandomDelay(windowMs)
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
// Optionally use random broadcast node
const node = this.config.useRandomBroadcastNode
? await this._selectRandomBroadcastNode()
: broadcaster
const txId = await node.broadcast(tx)
resolve(txId)
} catch (error) {
reject(error)
}
}, delay)
})
}
/**
* Batch schedule multiple broadcasts
* Ensures they're spread across the window
*/
async scheduleBatchBroadcast(
transactions: Transaction[],
windowMs: number,
broadcaster: TransactionBroadcaster,
): Promise<string[]> {
// Assign random delays to each
const scheduled = transactions.map(tx => ({
tx,
delay: this.getRandomDelay(windowMs),
}))
// Sort by delay for efficient scheduling
scheduled.sort((a, b) => a.delay - b.delay)
// Execute in order
const results: string[] = []
let lastDelay = 0
for (const { tx, delay } of scheduled) {
const waitTime = delay - lastDelay
await sleep(waitTime)
const txId = await broadcaster.broadcast(tx)
results.push(txId)
lastDelay = delay
}
return results
}
/**
* Create decoy transaction (optional, high privacy)
*/
async createDecoyTransaction(
denomination: number,
feeRate: number,
): Promise<Transaction | null> {
if (!this.config.enableDecoys) {
return null
}
// Create self-send transaction that looks like SwapSig setup
// ... implementation
return null
}
}
```
**Deliverables**:
- [ ] Exponential delay distribution
- [ ] Batch broadcast scheduling
- [ ] Decoy transaction support
- [ ] Unit tests
---
### Phase 6: Pool & Protocol Management (Week 4-5)
**Files to Create**:
- `pool.ts` - Pool lifecycle management
- `protocol.ts` - Protocol state machine
**Key Implementation Points**:
```typescript
// protocol.ts
/**
* SwapSig Protocol State Machine
*
* Phases:
* 1. DISCOVERY - Find/create pool
* 2. REGISTRATION - Register inputs with ownership proof
* 3. COMMITMENT - Submit destination commitments
* 4. SETUP - Build and broadcast setup transactions
* 5. SETUP_CONFIRM - Wait for confirmations
* 6. REVEAL - Reveal destinations to settlement partners
* 7. SETTLEMENT - MuSig2 signing and broadcast
* 8. SETTLEMENT_CONFIRM - Wait for confirmations
* 9. COMPLETE - Cleanup
*/
export class SwapSigProtocol extends EventEmitter {
private pool: SwapPool
private coordinator: MuSig2P2PCoordinator
private discovery: SwapSigDiscovery
private commitment: SecureDestinationCommitment
private burn: StealthBurnMechanism
private timing: TimingObfuscation
/**
* Execute complete swap protocol
*/
async executeSwap(
input: SwapInput,
destination: Address,
): Promise<SwapResult> {
// Phase 1-2: Registration
await this._executeRegistration(input)
// Phase 3: Commitment
await this._executeCommitment(destination)
// Phase 4-5: Setup
await this._executeSetup()
// Phase 6: Reveal (to settlement partners only)
await this._executeReveal()
// Phase 7-8: Settlement
await this._executeSettlement()
return this._getResult()
}
/**
* Execute reveal phase with coordinator blindness
*/
private async _executeReveal(): Promise<void> {
this._transitionPhase(SwapPhase.REVEAL)
// Determine my settlement partners
const myIndex = this._getMyParticipantIndex()
const myGroup = this._getMyOutputGroup()
// Reveal destination ONLY to settlement partners
// NOT to coordinator or other participants
for (const partnerIndex of myGroup.participantIndices) {
if (partnerIndex === myIndex) continue
const partner = this.pool.participants[partnerIndex]
// Decrypt my destination for this partner
const encryptedDest = this._myCommitment.encryptedDestinations.get(
partner.peerId,
)
// Send reveal directly to partner (not broadcast)
await this.coordinator.sendTo(partner.peerId, {
type: 'swapsig:destination-reveal',
payload: {
poolId: this.pool.poolId,
participantIndex: myIndex,
destination: this._myDestination.toString(),
blindingFactor: this._myBlindingFactor.toString('hex'),
},
})
}
// Wait for reveals from partners
await this._waitForPartnerReveals()
// Verify all commitments
this._verifyAllReveals()
}
/**
* Execute settlement with three-phase MuSig2
*/
private async _executeSettlement(): Promise<void> {
this._transitionPhase(SwapPhase.SETTLEMENT)
// Get my output groups (groups I'm a signer for)
const myGroups = this._getMyOutputGroups()
for (const group of myGroups) {
// Build settlement transaction
const settlementTx = this._buildSettlementTransaction(group)
// Compute sighash
const sighash = this._computeSighash(settlementTx, group)
// Create signing request (Phase 2 of MuSig2 P2P)
const requestId = await this.coordinator.announceSigningRequest(
group.signers,
sighash,
this._myPrivateKey,
{
metadata: {
swapPoolId: this.pool.poolId,
groupIndex: group.groupIndex,
transactionType: 'swap-settlement',
},
},
)
group.signingRequestId = requestId
// Wait for session creation (all signers join)
const sessionId = await this._waitForSessionReady(requestId)
group.sessionId = sessionId
// MuSig2 rounds happen automatically via P2P coordinator
// Wait for completion
const signature = await this.coordinator.getFinalSignature(sessionId)
// Add signature to transaction
this._addSignature(settlementTx, signature)
// Broadcast with timing obfuscation
const txId = await this.timing.scheduleBroadcast(
settlementTx,
this.pool.privacyConfig.settlementBroadcastWindowMs,
this._broadcaster,
)
group.settlementTxId = txId
}
// Wait for all settlements to confirm
await this._waitForSettlementConfirmations()
this._transitionPhase(SwapPhase.COMPLETE)
}
}
```
**Deliverables**:
- [ ] Pool lifecycle management
- [ ] Protocol state machine
- [ ] Phase transition logic
- [ ] Event emission
- [ ] Unit tests
---
### Phase 7: Main Coordinator (Week 5-6)
**Files to Create**:
- `coordinator.ts` - Main SwapSigCoordinator class
**Implementation**:
```typescript
// coordinator.ts
import { EventEmitter } from 'events'
import type { MuSig2P2PCoordinator } from '../musig2/coordinator.js'
import { SwapSigDiscovery } from './discovery.js'
import { SwapSigProtocol } from './protocol.js'
import { SecureDestinationCommitment } from './commitment.js'
import { StealthBurnMechanism } from './burn.js'
import { TimingObfuscation } from './timing.js'
import type {
SwapSigConfig,
SwapPool,
SwapParticipant,
SwapResult,
PoolFilters,
} from './types.js'
/**
* SwapSig Coordinator
*
* Main entry point for SwapSig privacy protocol.
* Coordinates pool discovery, participant registration,
* and swap execution using MuSig2 P2P infrastructure.
*/
export class SwapSigCoordinator extends EventEmitter {
private musig2Coordinator: MuSig2P2PCoordinator
private config: Required<SwapSigConfig>
// Components
private discovery: SwapSigDiscovery
private burnMechanism: StealthBurnMechanism
private timing: TimingObfuscation
// Active pools
private pools: Map<string, SwapPool> = new Map()
private activeProtocols: Map<string, SwapSigProtocol> = new Map()
constructor(config: SwapSigConfig) {
super()
this.config = this._mergeWithDefaults(config)
this.musig2Coordinator = config.musig2Coordinator
// Initialize components
this.discovery = new SwapSigDiscovery(
this.musig2Coordinator,
this.config.privacyConfig,
)
this.burnMechanism = new StealthBurnMechanism(this.config.burnConfig)
this.timing = new TimingObfuscation(this.config.timingConfig)
// Setup event handlers
this._setupEventHandlers()
}
// ============================================================================
// Public API
// ============================================================================
/**
* Discover available swap pools
*/
async discoverPools(filters?: PoolFilters): Promise<PoolAnnouncement[]> {
return this.discovery.discoverPools(filters || {})
}
/**
* Get recommended pools (sorted by quality)
*/
async getRecommendedPools(denomination: number): Promise<PoolAnnouncement[]> {
const pools = await this.discoverPools({ denomination })
return pools
.filter(p => p.currentParticipants < p.maxParticipants)
.sort((a, b) => this._scorePool(b) - this._scorePool(a))
}
/**
* Create new swap pool
*/
async createPool(params: CreatePoolParams): Promise<string> {
// Generate random pool ID (not derived from creator)
const poolId = this._generateRandomPoolId()
// Create pool
const pool: SwapPool = {
poolId,
denomination: params.denomination,
minParticipants: params.minParticipants || 3,
maxParticipants: params.maxParticipants || 10,
feeRate: params.feeRate || 1,
privacyConfig: this.config.privacyConfig,
burnConfig: this.config.burnConfig,
participants: [],
participantMap: new Map(),
outputGroups: [],
settlementMapping: new Map(),
phase: SwapPhase.DISCOVERY,
createdAt: Date.now(),
aborted: false,
setupTimeout: params.setupTimeout || 600000,
settlementTimeout: params.settlementTimeout || 600000,
}
this.pools.set(poolId, pool)
// Announce anonymously
await this.discovery.announcePool(pool)
this.emit('pool:created', poolId, pool)
return poolId
}
/**
* Join existing pool
*/
async joinPool(
poolId: string,
input: SwapInput,
destination: Address,
): Promise<void> {
// Validate input
await this._validateInput(input)
// Create secure destination commitment
const { commitment, blindingFactor } =
SecureDestinationCommitment.commit(destination)
// Encrypt destination for all potential partners
const encryptedDestinations = new Map<string, Buffer>()
// (Will be populated as we learn about other participants)
// Create participant
const participant: SwapParticipant = {
peerId: this.musig2Coordinator.myPeerId,
participantIndex: -1, // Assigned by pool
publicKey: this._myPublicKey,
input,
ownershipProof: await this._createOwnershipProof(input, poolId),
destinationCommitment: {
commitment,
encryptedDestinations,
},
setupConfirmed: false,
burnVerified: false,
joinedAt: Date.now(),
}
// Store for later
this._myDestination = destination
this._myBlindingFactor = blindingFactor
// Join via discovery (anonymous)
await this.discovery.joinPool(poolId, participant)
this.emit('pool:joined', poolId)
}
/**
* Execute complete swap
*/
async executeSwap(
poolId: string,
input: SwapInput,
destination: Address,
): Promise<SwapResult> {
// Join pool
await this.joinPool(poolId, input, destination)
// Get or create protocol instance
let protocol = this.activeProtocols.get(poolId)
if (!protocol) {
protocol = new SwapSigProtocol(
this.pools.get(poolId)!,
this.musig2Coordinator,
this.discovery,
this.burnMechanism,
this.timing,
)
this.activeProtocols.set(poolId, protocol)
}
// Execute swap
const result = await protocol.executeSwap(input, destination)
// Cleanup
this.activeProtocols.delete(poolId)
return result
}
/**
* Get pool status
*/
getPoolStatus(poolId: string): PoolStatus | undefined {
const pool = this.pools.get(poolId)
if (!pool) return undefined
return {
poolId,
phase: pool.phase,
participants: pool.participants.length,
minRequired: pool.minParticipants,
maxAllowed: pool.maxParticipants,
setupComplete: pool.phase >= SwapPhase.SETUP_CONFIRM,
settlementsComplete: pool.phase === SwapPhase.COMPLETE,
healthy: !pool.aborted,
}
}
// ============================================================================
// Private Methods
// ============================================================================
private _setupEventHandlers(): void {
// Handle signing requests for SwapSig settlements
this.musig2Coordinator.on('signing-request:received', async request => {
if (request.metadata?.transactionType !== 'swap-settlement') return
const poolId = request.metadata.swapPoolId
const protocol = this.activeProtocols.get(poolId)
if (protocol) {
await protocol.handleSigningRequest(request)
}
})
// Handle session ready events
this.musig2Coordinator.on('session:ready', (sessionId, requestId) => {
// Forward to relevant protocol
for (const protocol of this.activeProtocols.values()) {
protocol.handleSessionReady(sessionId, requestId)
}
})
}
private _generateRandomPoolId(): string {
// Random 32-byte ID (not derived from creator identity)
return Hash.sha256(
Buffer.concat([
Buffer.from(Date.now().toString()),
Buffer.from(Math.random().toString()),
PrivateKey.fromRandom().toBuffer(),
]),
).toString('hex')
}
private _scorePool(pool: PoolAnnouncement): number {
// Score based on:
// - Participant count (more = better anonymity)
// - Time remaining (more = better)
// - Group size strategy (5-of-5 preferred)
let score = 0
score += pool.currentParticipants * 10
score += (pool.expiresAt - Date.now()) / 60000 // Minutes remaining
if (pool.currentParticipants >= 15 && pool.currentParticipants <= 49) {
score += 50 // Sweet spot for 5-of-5
}
return score
}
}
```
**Deliverables**:
- [ ] Main coordinator class
- [ ] Public API implementation
- [ ] Event handling
- [ ] Integration with MuSig2 P2P
- [ ] Unit tests
---
### Phase 8: Validation & Security (Week 6)
**Files to Create**:
- `validation.ts` - Input/output validation
- `security.ts` - Security mechanisms
**Deliverables**:
- [ ] Input validation
- [ ] Output validation
- [ ] Burn verification
- [ ] Ownership proof validation
- [ ] Rate limiting
- [ ] Unit tests
---
### Phase 9: Integration Testing (Week 7)
**Test Files to Create**:
```
test/p2p/swapsig/
├── commitment.test.ts
├── burn.test.ts
├── discovery.test.ts
├── timing.test.ts
├── protocol.test.ts
├── coordinator.test.ts
├── integration.test.ts
├── privacy.test.ts
└── security.test.ts
```
**Test Scenarios**:
- [ ] 3-party swap (2-of-2 groups)
- [ ] 5-party swap (2-of-2 groups)
- [ ] 10-party swap (3-of-3 groups)
- [ ] 25-party swap (5-of-5 groups)
- [ ] Participant abandonment
- [ ] Coordinator failover
- [ ] Timing obfuscation verification
- [ ] Burn verification
- [ ] Commitment scheme security
- [ ] DHT privacy verification
- [ ] Intersection attack prevention [NEW]
- [ ] Fee rate fingerprinting resistance [NEW]
- [ ] Amount correlation resistance [NEW]
- [ ] Timelock refund execution [NEW]
- [ ] Decoy transaction generation [NEW]
- [ ] Privacy mode switching [NEW]
---
### Phase 10: Wallet Integration & Intersection Prevention (Week 6-7) [NEW]
**Files to Create**:
- `wallet.ts` - SwapSig wallet integration
**Implementation**:
```typescript
// wallet.ts
/**
* SwapSig Wallet Integration
*
* Prevents intersection attacks by:
* 1. Tracking which outputs came from which pools
* 2. Preventing combination of outputs from different pools
* 3. Enforcing output cooldown periods
*/
export class SwapSigWallet {
// Track output origins
private outputPoolMapping: Map<string, PoolInfo> = new Map()
// Cooldown tracking
private outputLastUsed: Map<string, number> = new Map()
/**
* Register output from completed swap
*/
registerSwapOutput(utxo: UTXO, poolInfo: PoolInfo): void {
const key = `${utxo.txid}:${utxo.vout}`
this.outputPoolMapping.set(key, poolInfo)
this.outputLastUsed.set(key, Date.now())
}
/**
* Validate transaction doesn't create intersection risk
*/
validateTransactionPrivacy(inputs: UTXO[]): PrivacyRisk {
const pools = new Set<string>()
for (const input of inputs) {
const key = `${input.txid}:${input.vout}`
const poolInfo = this.outputPoolMapping.get(key)
if (poolInfo) {
pools.add(poolInfo.poolId)
}
}
if (pools.size > 1) {
return {
level: 'CRITICAL',
reason: 'INTERSECTION_ATTACK',
message: `Combining outputs from ${pools.size} different pools`,
pools: Array.from(pools),
}
}
return { level: 'SAFE', reason: 'NO_INTERSECTION' }
}
/**
* Check output cooldown
*/
isOutputCooledDown(utxo: UTXO, cooldownMs: number): boolean {
const key = `${utxo.txid}:${utxo.vout}`
const lastUsed = this.outputLastUsed.get(key)
if (!lastUsed) return true
return Date.now() - lastUsed >= cooldownMs
}
}
```
**Deliverables**:
- [ ] Output pool tracking
- [ ] Intersection risk detection
- [ ] Cooldown enforcement
- [ ] Wallet integration API
- [ ] Unit tests
---
### Phase 11: Timelock Refund Scripts (Week 7) [NEW]
**Files to Create**:
- `refund.ts` - Lotus OP_CHECKSEQUENCEVERIFY refund scripts
**Implementation**:
```typescript
// refund.ts
import { Script, Opcode } from '../../bitcore/index.js'
import type { PublicKey } from '../../bitcore/publickey.js'
/**
* Timelock Refund Scripts
*
* Uses Lotus OP_CHECKSEQUENCEVERIFY for trustless refunds.
* If settlement fails, original owner can reclaim after timeout.
*/
export class TimelockRefund {
/**
* Build refund script for Taproot script-spend path
*
* Script: <timeout> OP_CSV OP_DROP <pubkey> OP_CHECKSIG
*/
static buildRefundScript(
ownerPubkey: PublicKey,
timeoutBlocks: number, // e.g., 720 blocks = ~1 day on Lotus
): Script {
return new Script()
.add(Script.encodeNumber(timeoutBlocks))
.add(Opcode.OP_CHECKSEQUENCEVERIFY)
.add(Opcode.OP_DROP)
.add(ownerPubkey.toBuffer())
.add(Opcode.OP_CHECKSIG)
}
/**
* Build Taproot output with refund path
*/
static buildTaprootWithRefund(
musig2AggKey: PublicKey,
refundScript: Script,
): TaprootOutput {
// Compute tapleaf hash
const tapleafHash = Hash.taggedHash(
'TapLeaf',
Buffer.concat([
Buffer.from([0xc0]), // TAPROOT_LEAF_TAPSCRIPT
refundScript.toBuffer(),
]),
)
// Tweak aggregated key
const tweakHash = Hash.taggedHash(
'TapTweak',
Buffer.concat([musig2AggKey.toBuffer(), tapleafHash]),
)
const commitment = musig2AggKey.point.add(
Point.fromScalar(BN.fromBuffer(tweakHash)),
)
return {
commitment: new PublicKey(commitment),
internalKey: musig2AggKey,
tapleafHash,
refundScript,
}
}
/**
* Build refund transaction (script-spend path)
*/
static buildRefundTransaction(
taprootUtxo: UTXO,
refundScript: Script,
controlBlock: Buffer,
ownerPrivkey: PrivateKey,
destinationAddress: Address,
feeRate: number,
): Transaction {
const tx = new Transaction()
.from(taprootUtxo)
.to(destinationAddress, taprootUtxo.satoshis - estimateFee(feeRate))
// Set sequence for CSV
tx.inputs[0].sequenceNumber = timeoutBlocks
// Sign with owner key
const signature = this._signScriptSpend(tx, ownerPrivkey, refundScript)
// Build scriptSig for Taproot script-spend
tx.inputs[0].setScript(
new Script()
.add(signature)
.add(refundScript.toBuffer())
.add(controlBlock),
)
return tx
}
}
```
**Deliverables**:
- [ ] Refund script construction
- [ ] Taproot with script-spend path
- [ ] Refund transaction building
- [ ] CSV timeout handling
- [ ] Unit tests
---
### Phase 12: Fee & Amount Obfuscation (Week 7) [NEW]
**Files to Create**:
- `obfuscation.ts` - Fee rate and amount randomization
**Implementation**:
```typescript
// obfuscation.ts
/**
* Fee & Amount Obfuscation
*
* Prevents fingerprinting via:
* 1. Randomized fee rates (±20%)
* 2. Slight amount variance (±1%)
* 3. Random padding for burns
*/
export class Obfuscation {
private config: ObfuscationConfig
constructor(config: ObfuscationConfig) {
this.config = config
}
/**
* Get randomized fee rate
*/
getRandomizedFeeRate(baseFeeRate: number): number {
if (!this.config.enableFeeRandomization) {
return baseFeeRate
}
const variance = baseFeeRate * this.config.feeVariancePercent
const offset = (Math.random() - 0.5) * 2 * variance
return Math.max(1, baseFeeRate + offset)
}
/**
* Get randomized amount
*/
getRandomizedAmount(baseAmount: number): number {
if (!this.config.enableAmountVariance) {
return baseAmount
}
const variance = baseAmount * this.config.amountVariancePercent
const offset = (Math.random() - 0.5) * 2 * variance
return Math.floor(baseAmount + offset)
}
/**
* Get burn amount with random padding
*/
getBurnWithPadding(baseBurn: number): number {
if (!this.config.enableBurnPadding) {
return baseBurn
}
const padding = Math.random() * baseBurn * this.config.burnPaddingPercent
return Math.floor(baseBurn + padding)
}
}
export interface ObfuscationConfig {
enableFeeRandomization: boolean
feeVariancePercent: number // e.g., 0.2 = ±20%
enableAmountVariance: boolean
amountVariancePercent: number // e.g., 0.01 = ±1%
enableBurnPadding: boolean
burnPaddingPercent: number // e.g., 0.5 = 0-50% extra
}
```
**Deliverables**:
- [ ] Fee rate randomization
- [ ] Amount variance
- [ ] Burn padding
- [ ] Configuration interface
- [ ] Unit tests
---
### Phase 13: Decoy Transactions (Week 7-8) [NEW]
**Files to Create**:
- `decoy.ts` - Decoy transaction generation
**Implementation**:
```typescript
// decoy.ts
/**
* Decoy Transaction Generator
*
* Creates self-send transactions that look like SwapSig
* to obscure real SwapSig transactions in the graph.
*/
export class DecoyGenerator {
/**
* Create decoy setup transaction
*
* Looks like: input → Taproot output
* But is actually: self-send to own Taproot address
*/
async createDecoySetup(
wallet: SwapSigWallet,
denomination: number,
feeRate: number,
): Promise<Transaction> {
// Select input matching denomination
const input = await wallet.selectUTXO(denomination)
// Generate new Taproot address (self-owned)
const taprootAddress = await wallet.getNewTaprootAddress()
// Build transaction that looks like SwapSig setup
const tx = new Transaction()
.from(input)
.to(taprootAddress, denomination - estimateFee(feeRate))
// Sign with wallet key
tx.sign(wallet.privateKey)
return tx
}
/**
* Create decoy settlement transaction
*/
async createDecoySettlement(
wallet: SwapSigWallet,
taprootUtxo: UTXO,
feeRate: number,
): Promise<Transaction> {
// Get new regular address
const finalAddress = await wallet.getNewAddress()
// Build transaction that looks like SwapSig settlement
const tx = new Transaction()
.from(taprootUtxo)
.to(finalAddress, taprootUtxo.satoshis - estimateFee(feeRate))
// Sign (key-spend path)
tx.sign(wallet.privateKey)
return tx
}
/**
* Schedule decoy broadcasts around real SwapSig
*/
async scheduleDecoys(
realTxs: Transaction[],
decoyRate: number, // e.g., 0.2 = 20% decoys
broadcastWindowMs: number,
): Promise<void> {
const decoyCount = Math.floor(realTxs.length * decoyRate)
for (let i = 0; i < decoyCount; i++) {
const delay = Math.random() * broadcastWindowMs
const decoyTx = await this.createDecoySetup(...)
setTimeout(() => {
this.broadcast(decoyTx)
}, delay)
}
}
}
```
**Deliverables**:
- [ ] Decoy setup transactions
- [ ] Decoy settlement transactions
- [ ] Decoy scheduling
- [ ] Integration with timing obfuscation
- [ ] Unit tests
---
### Phase 14: Documentation & Examples (Week 8)
**Files to Create**:
- `README.md` - Module documentation
- `examples/swapsig-basic.ts`
- `examples/swapsig-advanced.ts`
- `examples/swapsig-privacy-config.ts`
- `examples/swapsig-wallet-integration.ts` [NEW]
- `examples/swapsig-refund.ts` [NEW]
---
## File Summary
| File | Lines (Est.) | Priority | Week | Notes |
| ---------------- | ------------ | -------- | ------- | ------------------------------ |
| `types.ts` | ~500 | P0 | 1 | +privacy modes, +wallet types |
| `errors.ts` | ~100 | P0 | 1 | |
| `index.ts` | ~50 | P0 | 1 | |
| `commitment.ts` | ~300 | P0 | 2 | |
| `burn.ts` | ~250 | P0 | 2-3 | |
| `discovery.ts` | ~350 | P0 | 3 | |
| `timing.ts` | ~200 | P1 | 3-4 | |
| `pool.ts` | ~400 | P0 | 4-5 | |
| `protocol.ts` | ~600 | P0 | 4-5 | |
| `coordinator.ts` | ~500 | P0 | 5-6 | |
| `validation.ts` | ~300 | P1 | 6 | |
| `security.ts` | ~200 | P1 | 6 | |
| `wallet.ts` | ~350 | P0 | 6-7 | [NEW] Intersection prevention |
| `refund.ts` | ~250 | P1 | 7 | [NEW] Lotus CSV timelocks |
| `obfuscation.ts` | ~200 | P1 | 7 | [NEW] Fee/amount randomization |
| `decoy.ts` | ~150 | P2 | 7-8 | [NEW] Decoy transactions |
| `README.md` | ~300 | P2 | 8 | |
| **Total** | **~5,000** | - | 8 weeks | +30% from v2 requirements |
---
## Dependencies
### Required (Existing)
- `lib/p2p/musig2/coordinator.ts` - MuSig2 P2P coordination
- `lib/p2p/musig2/discovery-extension.ts` - Discovery layer
- `lib/bitcore/musig2/session.ts` - MuSig2 session management
- `lib/bitcore/crypto/` - Cryptographic primitives
- `lib/bitcore/transaction/` - Transaction construction
### New Dependencies
- None (uses existing infrastructure)
---
## Success Criteria
1. **Privacy**: No on-chain protocol fingerprinting
2. **Security**: All gaps from v1 and v2 security analysis addressed
3. **Performance**: 5-party swap completes in <10 minutes
4. **Reliability**: 95%+ success rate for swaps
5. **Testing**: >90% code coverage
6. **Intersection Prevention**: Wallet integration prevents output mixing [NEW]
7. **Trustless Refunds**: Timelock refunds work without coordinator [NEW]
8. **Fingerprint Resistance**: Fee/amount obfuscation passes analysis [NEW]
---
## Risk Mitigation
| Risk | Mitigation |
| ------------------------------ | -------------------------------------------- |
| Pedersen commitment complexity | Use existing Point/BN primitives |
| Onion routing complexity | Start with Tor integration, add custom later |
| Timing analysis | Exponential distribution is proven effective |
| DHT privacy | Anonymous announcements + Tor queries |
| Intersection attacks | Wallet-level output isolation [NEW] |
| Fee fingerprinting | ±20% fee variance [NEW] |
| Amount correlation | ±1% amount variance [NEW] |
| Stuck funds | Lotus CSV timelock refunds [NEW] |
| Graph analysis | Decoy transactions + pool chaining [NEW] |
---
## Lotus-Specific Features Used