From efceeb217b4df6d956d9fb57c6035ae21f22f8f8 Mon Sep 17 00:00:00 2001 From: Leonardo Saturnino Date: Sun, 26 Oct 2025 22:33:58 -0400 Subject: [PATCH 1/2] feat(scripts): add wallet-operator mapper for consolidation analysis Add comprehensive wallet-operator mapping toolset for beta staker consolidation analysis. Enables identification of wallets containing deprecated operators and BTC distribution calculations. Core functionality: - query-dkg-events.js: Extracts operator membership from on-chain DKG events - analyze-per-operator.js: Calculates BTC distribution by provider - validate-operator-list.js: Verifies operator list completeness Configuration: - operators.json: Defines KEEP (4 active) vs DISABLE (16 deprecated) operators - Contract ABIs for Bridge and WalletRegistry interactions - Archive node RPC support for historical event queries Documentation: - README: Usage guide and integration points - Manual sweep procedures and execution scripts - Operator consolidation communication guidelines Integration: - Provides data source for monitoring dashboard - Supports draining progress assessment - Enables manual sweep decision-making Technical details: - Uses threshold cryptography (51/100 signatures) - Queries sortition pool for operator address resolution - Classifies operators by provider (STAKED, P2P, BOAR, NUCO) --- scripts/wallet-operator-mapper/.env.example | 13 + scripts/wallet-operator-mapper/.gitignore | 13 + scripts/wallet-operator-mapper/README.md | 89 + .../analyze-per-operator.js | 195 ++ .../contracts/Bridge.json | 71 + .../contracts/WalletRegistry.json | 29 + ...25-10-10-manual-sweep-technical-process.md | 2945 +++++++++++++++++ ...10-operator-consolidation-communication.md | 496 +++ .../docs/2025-10-20-manual-sweep-README.md | 358 ++ ...025-10-20-manual-sweep-execution-script.sh | 537 +++ scripts/wallet-operator-mapper/operators.json | 131 + scripts/wallet-operator-mapper/package.json | 26 + .../query-dkg-events.js | 413 +++ .../validate-operator-list.js | 251 ++ 14 files changed, 5567 insertions(+) create mode 100644 scripts/wallet-operator-mapper/.env.example create mode 100644 scripts/wallet-operator-mapper/.gitignore create mode 100644 scripts/wallet-operator-mapper/README.md create mode 100644 scripts/wallet-operator-mapper/analyze-per-operator.js create mode 100644 scripts/wallet-operator-mapper/contracts/Bridge.json create mode 100644 scripts/wallet-operator-mapper/contracts/WalletRegistry.json create mode 100644 scripts/wallet-operator-mapper/docs/2025-10-10-manual-sweep-technical-process.md create mode 100644 scripts/wallet-operator-mapper/docs/2025-10-10-operator-consolidation-communication.md create mode 100644 scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-README.md create mode 100755 scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-execution-script.sh create mode 100644 scripts/wallet-operator-mapper/operators.json create mode 100644 scripts/wallet-operator-mapper/package.json create mode 100644 scripts/wallet-operator-mapper/query-dkg-events.js create mode 100644 scripts/wallet-operator-mapper/validate-operator-list.js diff --git a/scripts/wallet-operator-mapper/.env.example b/scripts/wallet-operator-mapper/.env.example new file mode 100644 index 0000000000..df0013d849 --- /dev/null +++ b/scripts/wallet-operator-mapper/.env.example @@ -0,0 +1,13 @@ +# Ethereum RPC Endpoint +# Get free endpoints from: +# - Infura: https://infura.io +# - Alchemy: https://alchemy.com +# - Public: https://ethereum.publicnode.com (rate limited) + +ETHEREUM_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID + +# Optional: Query delay to avoid rate limiting (milliseconds) +QUERY_DELAY_MS=100 + +# Optional: Enable verbose logging +DEBUG=false diff --git a/scripts/wallet-operator-mapper/.gitignore b/scripts/wallet-operator-mapper/.gitignore new file mode 100644 index 0000000000..d78a783a19 --- /dev/null +++ b/scripts/wallet-operator-mapper/.gitignore @@ -0,0 +1,13 @@ +# Environment +.env + +# Dependencies +node_modules/ +package-lock.json + +# Output +wallet-operator-mapping.json + +# Logs +*.log +npm-debug.log* diff --git a/scripts/wallet-operator-mapper/README.md b/scripts/wallet-operator-mapper/README.md new file mode 100644 index 0000000000..f3dc167f7e --- /dev/null +++ b/scripts/wallet-operator-mapper/README.md @@ -0,0 +1,89 @@ +# Wallet-Operator Mapper + +**Purpose**: Maps tBTC v2 wallets to their controlling operators for beta staker consolidation. + +## Quick Start + +```bash +# 1. Install dependencies +npm install + +# 2. Configure RPC endpoint +cp .env.example .env +# Edit .env with your Alchemy/Infura archive node URL + +# 3. Run analysis +node analyze-per-operator.js +``` + +## What This Does + +Analyzes tBTC wallets to identify which contain deprecated operators being removed during consolidation. + +**Core Function**: Queries on-chain DKG (Distributed Key Generation) events to extract the 100 operators controlling each wallet, then classifies them as KEEP (active) or DISABLE (deprecated) based on `operators.json`. + +## Main Scripts + +### query-dkg-events.js +Queries on-chain DKG events to extract wallet operator membership. +- **Requirements**: Archive node RPC (Alchemy recommended) +- **Runtime**: ~20 seconds per wallet +- **Output**: `wallet-operator-mapping.json` +- **Usage**: Run when wallet data needs updating + +### analyze-per-operator.js +Calculates BTC distribution by provider from mapping data. +- **Runtime**: <1 second +- **Output**: Console report with per-provider BTC analysis +- **Usage**: Run after query-dkg-events.js to analyze results + +### validate-operator-list.js +Verifies operator list completeness against CSV data. +- **Purpose**: Data quality checks +- **Usage**: Optional validation step + +## Configuration Files + +### operators.json ⭐ CRITICAL +Defines which operators to keep vs disable during consolidation. + +**Structure**: +- `operators.keep[]`: 4 active operators (1 per provider: STAKED, P2P, BOAR, NUCO) +- `operators.disable[]`: 16 deprecated operators being removed + +**Purpose**: Used by scripts to tag discovered operators as KEEP or DISABLE. Without this file, scripts cannot classify operators or calculate BTC in deprecated wallets. + +**Source**: Memory Bank `/knowledge/8-final-operator-consolidation-list.md` + +### .env +RPC endpoint configuration. Archive node required for historical DKG event queries. + +## Output Data + +**wallet-operator-mapping.json** contains: +- Wallet metadata (PKH, BTC balance, state) +- Complete operator membership (100 operators per wallet) +- Operator addresses matched to providers +- KEEP/DISABLE status per operator +- Summary statistics by provider + +## Integration + +**Part of**: Beta Staker Consolidation (Memory Bank: `/memory-bank/20250809-beta-staker-consolidation/`) + +**Used for**: +- Monitoring dashboard data source (load mapping into Prometheus) +- Draining progress assessment (identify wallets requiring manual sweeps) +- Operator removal validation (verify wallets empty before removal) + +## Important Notes + +- **Equal-split calculation** is for analysis onlyβ€”operators hold cryptographic key shares, not BTC shares +- All wallets require 51/100 threshold signatures for transactions +- Manual sweeps need coordination from all 4 providers simultaneously +- Deprecated operators cannot be removed until their wallets reach 0 BTC + +## Documentation + +- `docs/` - Manual sweep procedures and technical processes +- See Memory Bank for complete consolidation planning and correlation analysis diff --git a/scripts/wallet-operator-mapper/analyze-per-operator.js b/scripts/wallet-operator-mapper/analyze-per-operator.js new file mode 100644 index 0000000000..fa0ec1df44 --- /dev/null +++ b/scripts/wallet-operator-mapper/analyze-per-operator.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node + +/** + * Proper Per-Operator BTC Analysis + * + * Calculates actual BTC share per operator/provider + */ + +const fs = require('fs'); +const path = require('path'); + +const MAPPING_FILE = path.join(__dirname, 'wallet-operator-mapping.json'); +const OPERATORS_FILE = path.join(__dirname, 'operators.json'); + +const data = JSON.parse(fs.readFileSync(MAPPING_FILE)); +const operatorsConfig = JSON.parse(fs.readFileSync(OPERATORS_FILE)); + +console.log('πŸ” Proper BTC Analysis by Operator/Provider\n'); +console.log('='.repeat(80)); + +// Get wallets with operator data +const walletsWithOps = data.wallets.filter(w => w.memberCount > 0); + +console.log(`\nTotal wallets with operator data: ${walletsWithOps.length}`); +console.log(`Total BTC in these wallets: ${walletsWithOps.reduce((s, w) => s + w.btcBalance, 0).toFixed(8)} BTC`); + +// Method 1: Per-wallet breakdown showing which providers are involved +console.log('\n' + '='.repeat(80)); +console.log('METHOD 1: Per-Wallet Provider Involvement'); +console.log('='.repeat(80)); + +const walletBreakdown = walletsWithOps.map(wallet => { + const deprecatedOps = wallet.operators.filter(op => op.status === 'DISABLE'); + const providers = new Set(deprecatedOps.map(op => op.provider)); + + return { + walletPKH: wallet.walletPKH, + btcBalance: wallet.btcBalance, + totalOperators: wallet.memberCount, + deprecatedCount: deprecatedOps.length, + providersInvolved: Array.from(providers).sort(), + activeOperators: wallet.operators.filter(op => op.status === 'KEEP').length + }; +}); + +// Group by provider combination +const providerGroups = {}; +walletBreakdown.forEach(w => { + const key = w.providersInvolved.join('+'); + if (!providerGroups[key]) { + providerGroups[key] = { + providers: w.providersInvolved, + wallets: [], + totalBTC: 0 + }; + } + providerGroups[key].wallets.push(w); + providerGroups[key].totalBTC += w.btcBalance; +}); + +console.log('\nWallets grouped by provider involvement:\n'); +Object.entries(providerGroups).forEach(([key, group]) => { + console.log(`Providers: ${group.providers.join(', ') || 'None'}`); + console.log(` Wallets: ${group.wallets.length}`); + console.log(` Total BTC: ${group.totalBTC.toFixed(8)} BTC`); + console.log(` Average BTC per wallet: ${(group.totalBTC / group.wallets.length).toFixed(2)} BTC`); + console.log(); +}); + +// Method 2: Equal-split calculation (BTC / operators in wallet) +console.log('='.repeat(80)); +console.log('METHOD 2: Equal-Split Per-Operator Share'); +console.log('='.repeat(80)); +console.log('\nAssumption: BTC is split equally among all 100 operators in each wallet\n'); + +const operatorShares = {}; + +// Initialize +operatorsConfig.operators.keep.forEach(op => { + operatorShares[op.address.toLowerCase()] = { + provider: op.provider, + status: 'KEEP', + totalShare: 0, + walletCount: 0 + }; +}); + +operatorsConfig.operators.disable.forEach(op => { + operatorShares[op.address.toLowerCase()] = { + provider: op.provider, + status: 'DISABLE', + totalShare: 0, + walletCount: 0 + }; +}); + +// Calculate shares +walletsWithOps.forEach(wallet => { + const sharePerOperator = wallet.btcBalance / wallet.memberCount; + + wallet.operators.forEach(op => { + const addr = op.address.toLowerCase(); + if (operatorShares[addr]) { + operatorShares[addr].totalShare += sharePerOperator; + operatorShares[addr].walletCount++; + } + }); +}); + +// Group by provider +const providerShares = { + STAKED: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 }, + P2P: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 }, + BOAR: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 }, + NUCO: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 } +}; + +Object.entries(operatorShares).forEach(([addr, data]) => { + if (providerShares[data.provider]) { + if (data.status === 'KEEP') { + providerShares[data.provider].keep += data.totalShare; + providerShares[data.provider].keepWallets = data.walletCount; + } else { + providerShares[data.provider].disable += data.totalShare; + providerShares[data.provider].disableWallets = data.walletCount; + } + } +}); + +console.log('Per-Provider BTC Shares (Equal-Split Method):\n'); +console.log('Provider | KEEP Ops Share | DISABLE Ops Share | Total Share'); +console.log('-'.repeat(80)); + +Object.entries(providerShares).forEach(([provider, shares]) => { + const total = shares.keep + shares.disable; + console.log(`${provider.padEnd(8)} | ${shares.keep.toFixed(2).padStart(13)} BTC | ${shares.disable.toFixed(2).padStart(17)} BTC | ${total.toFixed(2)} BTC`); +}); + +console.log('\n' + '='.repeat(80)); +console.log('METHOD 3: Deprecated Operator BTC (What needs to be "moved")'); +console.log('='.repeat(80)); +console.log('\nThis shows the BTC share held by deprecated operators that'); +console.log('needs to remain accessible during the draining period.\n'); + +Object.entries(providerShares).forEach(([provider, shares]) => { + console.log(`${provider}:`); + console.log(` Deprecated operator share: ${shares.disable.toFixed(2)} BTC`); + console.log(` Number of wallets involved: ${shares.disableWallets}`); + console.log(` Average per wallet: ${shares.disableWallets > 0 ? (shares.disable / shares.disableWallets).toFixed(2) : 0} BTC`); + console.log(); +}); + +// Method 4: Detailed operator-by-operator breakdown +console.log('='.repeat(80)); +console.log('METHOD 4: Individual Operator Breakdown (Top 20 by BTC)'); +console.log('='.repeat(80)); + +const operatorList = Object.entries(operatorShares) + .map(([addr, data]) => ({ + address: addr, + ...data + })) + .sort((a, b) => b.totalShare - a.totalShare) + .slice(0, 20); + +console.log('\nOperator Address | Provider | Status | BTC Share | Wallets'); +console.log('-'.repeat(80)); +operatorList.forEach(op => { + const addrShort = op.address.slice(0, 10) + '...' + op.address.slice(-6); + console.log(`${addrShort} | ${op.provider.padEnd(7)} | ${op.status.padEnd(7)} | ${op.totalShare.toFixed(2).padStart(8)} BTC | ${op.walletCount}`); +}); + +// Summary +console.log('\n' + '='.repeat(80)); +console.log('SUMMARY & INTERPRETATION'); +console.log('='.repeat(80)); + +const totalBTC = walletsWithOps.reduce((s, w) => s + w.btcBalance, 0); +const totalDeprecatedShare = Object.values(providerShares).reduce((s, p) => s + p.disable, 0); + +console.log(`\nTotal BTC in analyzed wallets: ${totalBTC.toFixed(8)} BTC`); +console.log(`Total BTC share of deprecated operators: ${totalDeprecatedShare.toFixed(2)} BTC`); +console.log(`Percentage held by deprecated operators: ${(totalDeprecatedShare / totalBTC * 100).toFixed(2)}%`); + +console.log('\n⚠️ IMPORTANT NOTES:'); +console.log('1. The "equal-split" is a CALCULATION METHOD, not how threshold signatures work'); +console.log('2. In reality, ALL 100 operators must participate for wallet actions (51/100 threshold)'); +console.log('3. The deprecated operators do not individually "own" their share'); +console.log('4. What matters: which WALLETS contain deprecated operators (all 24 do)'); +console.log('5. For sweeps: need active operators to coordinate, not individual BTC shares'); + +console.log('\nβœ… CORRECT INTERPRETATION:'); +console.log('- All 24 wallets (5,923.91 BTC) contain deprecated operators'); +console.log('- Natural draining or manual sweeps affect the ENTIRE wallet, not per-operator'); +console.log('- Coordination needed: active operators from STAKED, P2P, BOAR (and NUCO for 20 wallets)'); diff --git a/scripts/wallet-operator-mapper/contracts/Bridge.json b/scripts/wallet-operator-mapper/contracts/Bridge.json new file mode 100644 index 0000000000..77d841e399 --- /dev/null +++ b/scripts/wallet-operator-mapper/contracts/Bridge.json @@ -0,0 +1,71 @@ +{ + "contractAddress": "0x5e4861a80B55f035D899f66772117F00FA0E8e7B", + "abi": [ + { + "inputs": [ + { + "internalType": "bytes20", + "name": "walletPubKeyHash", + "type": "bytes20" + } + ], + "name": "wallets", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "ecdsaWalletID", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "mainUtxoHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "pendingRedemptionsValue", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "createdAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "movingFundsRequestedAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "closingStartedAt", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "pendingMovedFundsSweepRequestsCount", + "type": "uint32" + }, + { + "internalType": "enum Wallets.WalletState", + "name": "state", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "movingFundsTargetWalletsCommitmentHash", + "type": "bytes32" + } + ], + "internalType": "struct Wallets.Wallet", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/scripts/wallet-operator-mapper/contracts/WalletRegistry.json b/scripts/wallet-operator-mapper/contracts/WalletRegistry.json new file mode 100644 index 0000000000..61ffafba57 --- /dev/null +++ b/scripts/wallet-operator-mapper/contracts/WalletRegistry.json @@ -0,0 +1,29 @@ +{ + "contractAddress": "0x46d52E41C2F300BC82217Ce22b920c34995204eb", + "abi": [ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "ecdsaWalletID", + "type": "bytes32" + } + ], + "name": "getWalletMembers", + "outputs": [ + { + "internalType": "uint32[]", + "name": "", + "type": "uint32[]" + }, + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/scripts/wallet-operator-mapper/docs/2025-10-10-manual-sweep-technical-process.md b/scripts/wallet-operator-mapper/docs/2025-10-10-manual-sweep-technical-process.md new file mode 100644 index 0000000000..9f6ee49ac9 --- /dev/null +++ b/scripts/wallet-operator-mapper/docs/2025-10-10-manual-sweep-technical-process.md @@ -0,0 +1,2945 @@ +# Manual Sweep Process - Complete Technical Specification +## tBTC Beta Staker Consolidation (18 β†’ 3 Operators) + +**Document Date**: 2025-10-10 +**Version**: 1.0 +**Audience**: Technical operators, engineering team +**Purpose**: Detailed technical documentation of the manual BTC wallet sweep process + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [When Manual Sweeps Are Triggered](#when-manual-sweeps-are-triggered) +3. [Prerequisites](#prerequisites) +4. [Actors and Roles](#actors-and-roles) +5. [Technical Architecture](#technical-architecture) +6. [Step-by-Step Process](#step-by-step-process) +7. [Data Structures and Parameters](#data-structures-and-parameters) +8. [Bitcoin Transaction Details](#bitcoin-transaction-details) +9. [Ethereum Smart Contract Interactions](#ethereum-smart-contract-interactions) +10. [RFC-12 Coordination Protocol](#rfc-12-coordination-protocol) +11. [Failure Scenarios and Recovery](#failure-scenarios-and-recovery) +12. [Cost Analysis](#cost-analysis) +13. [Timing and Scheduling](#timing-and-scheduling) +14. [Security Considerations](#security-considerations) +15. [Monitoring and Verification](#monitoring-and-verification) +16. [Operator Instructions](#operator-instructions) + +--- + +## Overview + +### What is a Manual Sweep? + +A **manual sweep** (formally called **MovingFunds** in the tBTC protocol) is a coordinated operation that moves Bitcoin (BTC) from a deprecated wallet to an active wallet within the same provider organization. + +**Key Characteristics**: +- Uses existing tBTC MovingFunds mechanism (no new code) +- Requires threshold signing coordination (51 of 100 operators) +- Leverages RFC-12 decentralized coordination +- Moves BTC on Bitcoin blockchain, then proves it to Ethereum via SPV +- Provider-specific: Each provider sweeps their own wallets + +**Not a Manual Sweep**: +- ❌ User-initiated redemptions (those are automatic) +- ❌ Emergency wallet draining (different mechanism) +- ❌ Cross-provider BTC movement (doesn't happen) +- ❌ Forced migration to new wallet system (this is same system) + +--- + +### Why Manual Sweeps May Be Required + +**Primary Reason**: Natural draining (user redemptions) is insufficient to empty deprecated wallets within the October 2025 deadline. + +**Trigger Conditions**: +1. **Straggler Wallets**: Wallet retains >20% of original balance after 4 weeks of natural draining +2. **Timeline Pressure**: October deadline is <3 weeks away and wallets not empty +3. **Low Redemption Volume**: Redemption rate drops significantly below historical average +4. **Edge Cases**: Specific wallets not being selected by redemption algorithm + +**Probability**: 30-50% chance that some wallets will require manual sweeps (hybrid approach assumption). + +--- + +### What Moves During a Manual Sweep? + +**Assets Moving**: +- βœ… **Bitcoin (BTC)** - Physical BTC on Bitcoin blockchain +- ❌ **NOT T tokens** - No token staking involved +- ❌ **NOT tBTC** - tBTC remains with users, unaffected +- ❌ **NOT NFTs** - No wallet ownership tokens move + +**Path of BTC**: +``` +Deprecated Wallet (Bitcoin Address) + ↓ +Bitcoin Transaction (on-chain) + ↓ +Active Wallet (Bitcoin Address, same provider) + ↓ +SPV Proof to Ethereum (proves movement occurred) + ↓ +Ethereum Bridge Updates State (wallet balances) +``` + +--- + +## When Manual Sweeps Are Triggered + +### Timeline Context + +Manual sweeps occur during **Phase 3: Assessment & Potential Manual Sweeps** (Weeks 4-5). + +**Week 4 Assessment Checkpoint** (~2025-12-01): +- Evaluate draining progress across all 15 deprecated wallets +- Calculate % BTC drained via natural redemptions +- Analyze redemption volume trends (stable, declining, increasing) +- Project completion timeline based on current velocity + +**Decision Matrix**: + +| Draining Progress | Redemption Volume | Action | +|-------------------|-------------------|--------| +| >50% drained | Stable | Continue natural draining | +| 30-50% drained | Stable | Monitor closely, prepare manual sweep | +| <30% drained | Stable | **Trigger manual sweep** for stragglers | +| Any progress | Declining >30% | **Trigger manual sweep immediately** | + +--- + +### Specific Trigger Criteria + +**Threshold Team will initiate manual sweeps if ANY of the following are true**: + +1. **Individual Wallet Threshold**: + - Wallet has >20% of original balance after 4 weeks + - Example: Wallet started with 10 BTC, still has >2 BTC at Week 4 + +2. **Timeline Threshold**: + - Current date is >2025-12-10 (less than 3 weeks to year-end target) + - Any wallet still has >5% balance + +3. **Redemption Volume Drop**: + - Weekly redemption volume drops >30% below historical average + - Projection shows completion >Week 9 (beyond October target) + +4. **Wallet Stagnation**: + - Wallet balance hasn't decreased in 2+ weeks + - Indicates wallet not being selected by redemption algorithm + +--- + +### Notification to Operators + +When manual sweeps are triggered, operators will receive: + +**48 Hours Before Coordination**: +1. **Email notification** with: + - Which wallets require sweeps (wallet public key hashes) + - Target coordination window (specific Ethereum block range) + - Expected BTC amounts to be moved + - Required operator participation (which providers involved) + +2. **Slack/Discord message** in operator coordination channel: + - Summary of assessment results + - Decision to proceed with manual sweeps + - Link to coordination details + +3. **Calendar invitation**: + - Coordination window time (in operator local timezones) + - Duration: 100 blocks (~20 minutes) + - Link to runbook and instructions + +4. **Dashboard alert**: + - Visual indicator on monitoring dashboard + - Affected wallets highlighted + - Coordination countdown timer + +--- + +## Prerequisites + +### Technical Prerequisites + +Before manual sweeps can be executed, the following must be in place: + +#### 1. Ethereum Mainnet Infrastructure + +- **Allowlist Contract**: Deployed and configured +- **WalletRegistry Contract**: Optimized and operational +- **Bridge Contract**: Active and accepting SPV proofs +- **WalletProposalValidator Contract**: Deployed for validation + +**Verification**: +```bash +# Check contracts on Ethereum mainnet +cast call $ALLOWLIST_ADDRESS "stakingProviders(address)" $OPERATOR_ADDRESS +cast call $WALLET_REGISTRY_ADDRESS "wallets(bytes20)" $WALLET_PKH +``` + +#### 2. Bitcoin Network Access + +- **Electrum Servers**: Multiple servers configured for redundancy +- **Bitcoin Mempool Monitoring**: Real-time fee estimation via mempool.space +- **Bitcoin Block Explorer**: For transaction verification (e.g., blockchain.info) + +**Verification**: +```bash +# Test Electrum server connectivity +electrum-client get_balance $BITCOIN_ADDRESS +``` + +#### 3. Operator Nodes + +- **Minimum 10 of 18 operators online** (51% threshold) +- **Node health**: All participating operators' nodes operational +- **Coordination sync**: Operators synchronized to same Ethereum block height + +**Verification**: +```bash +# Check operator node status +curl http://operator-node:8080/health +# Should return: {"status": "healthy", "blockHeight": 12345678} +``` + +#### 4. Wallet State + +- **Wallet Active**: Deprecated wallet is still in LIVE state (not closed/terminated) +- **Wallet Has BTC**: Confirmed UTXOs exist in deprecated wallet +- **No Pending Actions**: No other wallet actions pending (redemptions, heartbeats) + +**Verification**: +```bash +# Query wallet state on-chain +cast call $WALLET_REGISTRY_ADDRESS "getWalletState(bytes20)" $WALLET_PKH +# Should return: 0 (LIVE) +``` + +--- + +### Organizational Prerequisites + +#### 1. Provider Coordination + +**Provider Communication**: +- Provider management notified of upcoming sweep +- Operators instructed to be available during coordination window +- Emergency contact information confirmed + +**Provider Acknowledgment Required**: +- Confirmation of operator availability (via email/Slack) +- Agreement on coordination timing +- Backup operators identified (if primary unavailable) + +#### 2. Cost Approval + +**DAO Treasury**: +- Governance approval for manual sweep costs +- Budget allocated for Bitcoin miner fees ($50-200 per wallet) +- Budget allocated for Ethereum gas fees ($100-300 per SPV proof) + +**Total Estimated Cost** (if all 15 wallets require sweeps): +- Minimum: 15 Γ— $150 = $2,250 +- Maximum: 15 Γ— $500 = $7,500 + +#### 3. Documentation and Runbook + +**Required Documentation**: +- βœ… This manual sweep technical specification +- βœ… Operator runbook (step-by-step instructions) +- βœ… Emergency rollback procedures +- βœ… Coordination window calendar + +--- + +## Actors and Roles + +### 1. Threshold Team (Coordination Initiator) + +**Responsibilities**: +- Monitor draining progress via dashboard +- Execute Week 4 assessment and make go/no-go decision +- Identify straggler wallets requiring sweeps +- Notify operators 48 hours in advance +- Calculate Bitcoin transaction fees (mempool analysis) +- Construct MovingFunds proposal parameters +- Submit SPV proofs to Ethereum after Bitcoin confirmations + +**Tools Used**: +- Grafana monitoring dashboard +- Bitcoin mempool monitoring (mempool.space) +- Ethereum contract interaction tools (cast, hardhat) + +**Key Personnel**: +- Engineering lead +- DevOps coordinator +- Provider liaisons (3 people, one per provider) + +--- + +### 2. Leader Operator (Proposes MovingFunds) + +**Selection**: Automatically determined by RFC-12 coordination algorithm at start of coordination window. + +**Calculation**: +```python +coordination_seed = sha256(wallet_PKH + safe_block_hash) +rng = RNG(coordination_seed) +shuffled_operators = rng.shuffle(all_active_operators) +leader = shuffled_operators[0] +``` + +Where: +- `safe_block_hash` = block hash at `coordination_block - 32` +- `all_active_operators` = list of 18 operators (before consolidation) + +**Responsibilities**: +1. Receive MovingFunds proposal parameters from Threshold team +2. Validate proposal locally (off-chain checks) +3. Construct CoordinationMessage with MovingFunds proposal +4. Broadcast CoordinationMessage to all follower operators +5. Initiate threshold signing ceremony +6. Broadcast completed Bitcoin transaction to Electrum servers + +**Leader Probability**: +- Before consolidation: 1/18 = 5.6% per coordination window +- After consolidation: 1/3 = 33.3% per coordination window + +--- + +### 3. Follower Operators (Validate and Sign) + +**Who**: All operators NOT selected as leader (17 operators before consolidation). + +**Responsibilities**: +1. Receive CoordinationMessage from leader +2. Validate leader identity (check signature matches expected leader) +3. Validate timing (message received within 80% of coordination window) +4. Validate MovingFunds proposal: + - **On-chain validation**: Call `WalletProposalValidator.validateMovingFundsProposal()` + - **Off-chain validation**: Check Bitcoin transaction structure, UTXOs exist, fee reasonable +5. If valid, participate in threshold signing ceremony +6. Sign partial signature share +7. Submit signature share to leader for aggregation + +**Minimum Required**: 51 out of 100 operators must sign (with 18 total operators, need ~10). + +--- + +### 4. Active Operator (Receives BTC) + +**Who**: The single operator from the provider who will remain active after consolidation. + +**For This Consolidation**: +- **BOAR**: `0xffb804c2de78576ad011f68a7df63d739b8c8155` +- **STAKED**: `0xf401aae8c639eb1638fd99b90eae8a4c54f9894d` +- **P2P**: `0xb074a3b960f29a1448a2dd4de95210ca492c18d4` + +**Responsibilities**: +- Maintain wallet that will receive swept BTC +- Monitor BTC arrival after transaction confirms +- Verify balance increase matches expected amount +- Report any discrepancies to Threshold team + +**Note**: Active operator's node ALSO participates in threshold signing (if selected as leader or follower). + +--- + +### 5. Deprecated Operator (Wallet Being Drained) + +**Who**: Operator whose wallet is being swept (one of the 15 deprecated operators). + +**Responsibilities**: +- Keep node online and operational during coordination window +- Participate in threshold signing if node is still active +- Monitor wallet balance decrease after transaction +- Confirm wallet reached 0 BTC (or minimal dust) +- Await approval for node decommissioning + +**After Sweep**: +- Wallet marked as empty +- Operator flagged for removal from allowlist +- Wait 1 week at 0 BTC (safety buffer) +- Receive decommissioning approval from Threshold team + +--- + +## Technical Architecture + +### System Components + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ETHEREUM MAINNET β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Allowlist β”‚ β”‚WalletRegistry β”‚ β”‚ Bridge β”‚ β”‚ +β”‚ β”‚ Contract β”‚ β”‚ Contract β”‚ β”‚ Contract β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Weight=0 β”‚ MovingFunds β”‚ SPV Proof β”‚ +β”‚ β”‚ β”‚ State Update β”‚ Verification β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–² β”‚ +β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ OPERATOR NODES β”‚ + β”‚ (18 total, need β”‚ + β”‚ ~10 for signing) β”‚ + β”‚ β”‚ + β”‚ RFC-12 Protocol: β”‚ + β”‚ - Leader Election β”‚ + β”‚ - Coordination β”‚ + β”‚ - Threshold ECDSA β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ Bitcoin Tx + β”‚ Broadcast + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ BITCOIN NETWORK β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Deprecated β”‚ ──────> β”‚Active Wallet β”‚ β”‚ +β”‚ β”‚ Wallet β”‚ BTC β”‚ (Same Prov.) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Move β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Via Electrum Servers β†’ Bitcoin Miners β†’ 6 Confirmations β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +### Data Flow Overview + +``` +[Threshold Team] + β”‚ + β”‚ 1. Calculate MovingFunds Parameters + β”‚ (Target wallet, UTXOs, fee rate) + β”‚ + β–Ό +[Coordination Window Opens] + β”‚ + β”‚ 2. Leader Selected (RFC-12 algorithm) + β”‚ + β–Ό +[Leader Operator] + β”‚ + β”‚ 3. Receive parameters, construct proposal + β”‚ + β”‚ 4. Broadcast CoordinationMessage + β”‚ + β–Ό +[Follower Operators] + β”‚ + β”‚ 5. Validate proposal (on-chain + off-chain) + β”‚ + β”‚ 6. Participate in threshold signing + β”‚ (Each produces signature share) + β”‚ + β–Ό +[Leader Operator] + β”‚ + β”‚ 7. Aggregate signatures (51+ shares) + β”‚ + β”‚ 8. Construct complete Bitcoin transaction + β”‚ + β”‚ 9. Broadcast to Bitcoin network + β”‚ + β–Ό +[Bitcoin Network] + β”‚ + β”‚ 10. Transaction propagates, miners include + β”‚ + β”‚ 11. Wait 6 confirmations (~1 hour) + β”‚ + β–Ό +[Threshold Team or Any Operator] + β”‚ + β”‚ 12. Construct SPV proof (tx + merkle proof) + β”‚ + β”‚ 13. Submit SPV proof to Ethereum Bridge + β”‚ + β–Ό +[Ethereum Bridge Contract] + β”‚ + β”‚ 14. Verify SPV proof + β”‚ + β”‚ 15. Update wallet balances on-chain + β”‚ + β”‚ 16. Mark deprecated wallet as empty + β”‚ + β–Ό +[Verification Complete] +``` + +--- + +## Step-by-Step Process + +### Phase 1: Preparation (Threshold Team) + +**Duration**: 1-2 hours before coordination window + +#### Step 1.1: Identify Straggler Wallets + +**Action**: Query all 15 deprecated wallets and identify which require manual sweeps. + +**Technical Details**: +```bash +# Query wallet balance on Bitcoin blockchain +bitcoin-cli getreceivedbyaddress $BITCOIN_ADDRESS 0 + +# Query wallet state on Ethereum +cast call $WALLET_REGISTRY_ADDRESS \ + "getWalletMainUtxo(bytes20)" \ + $WALLET_PKH +``` + +**Criteria**: Wallet has >20% of original balance after 4 weeks. + +**Output**: List of wallet public key hashes (PKH) requiring sweeps. + +Example: +``` +Straggler wallets identified: +- 0x1234...abcd (BOAR, 2.5 BTC remaining) +- 0x5678...efgh (STAKED, 3.1 BTC remaining) +- 0x9abc...ijkl (P2P, 1.8 BTC remaining) +``` + +--- + +#### Step 1.2: Calculate Bitcoin Transaction Fees + +**Action**: Check current Bitcoin mempool congestion and estimate appropriate fee rate. + +**Technical Details**: +```bash +# Check current mempool fee recommendations +curl https://mempool.space/api/v1/fees/recommended +``` + +**Output**: +```json +{ + "fastestFee": 15, // sat/vB for next block + "halfHourFee": 12, // sat/vB for ~30 min + "hourFee": 10, // sat/vB for ~1 hour + "economyFee": 8 // sat/vB for >1 hour +} +``` + +**Decision**: Choose fee rate based on urgency. +- **Urgent** (deadline <1 week): Use `fastestFee` (higher cost) +- **Normal** (deadline 1-3 weeks): Use `halfHourFee` (balanced) +- **Not urgent** (deadline >3 weeks): Use `hourFee` (economical) + +**Fee Calculation**: +``` +Transaction Size Estimate: +- Input: 148 bytes per UTXO (P2PKH) or 68 bytes (P2WPKH) +- Output: 34 bytes per output +- Overhead: 10 bytes + +Typical MovingFunds transaction: +- 2 inputs Γ— 148 bytes = 296 bytes +- 1 output Γ— 34 bytes = 34 bytes +- Overhead = 10 bytes +- Total = 340 bytes + +Fee = 340 bytes Γ— 12 sat/vB = 4,080 sats β‰ˆ 0.0000408 BTC + +At $50,000/BTC: 0.0000408 Γ— $50,000 = $2.04 +``` + +**Note**: Actual fees vary based on: +- Number of UTXOs in deprecated wallet (more UTXOs = larger transaction) +- Wallet address type (P2PKH vs P2WPKH) +- Mempool congestion at time of broadcast + +--- + +#### Step 1.3: Query Wallet UTXOs + +**Action**: Identify all unspent transaction outputs (UTXOs) in deprecated wallet. + +**Technical Details**: +```bash +# Using Electrum client +electrum-client listunspent $BITCOIN_ADDRESS + +# Or via Bitcoin Core RPC +bitcoin-cli listunspent 0 9999999 '["$BITCOIN_ADDRESS"]' +``` + +**Output**: +```json +[ + { + "txid": "abc123...", + "vout": 0, + "address": "bc1q...", + "scriptPubKey": "0014...", + "amount": 1.5, + "confirmations": 100, + "spendable": true + }, + { + "txid": "def456...", + "vout": 1, + "address": "bc1q...", + "scriptPubKey": "0014...", + "amount": 1.0, + "confirmations": 50, + "spendable": true + } +] +``` + +**UTXO Selection Strategy**: +- Include all UTXOs (minimize remaining dust) +- Prioritize larger UTXOs first (if transaction size matters) +- Ensure UTXOs have sufficient confirmations (>6 confirmations) + +--- + +#### Step 1.4: Construct MovingFunds Proposal Parameters + +**Action**: Package all information needed for MovingFunds proposal. + +**Data Structure**: +```typescript +interface MovingFundsParameters { + // Wallet being drained + sourceWalletPKH: bytes20; // 20-byte wallet public key hash + + // Target wallet (same provider) + targetWalletPKH: bytes20; // 20-byte active wallet PKH + + // Bitcoin transaction inputs + utxos: Array<{ + txHash: bytes32; // Previous transaction hash + outputIndex: uint32; // Output index in that transaction + value: uint64; // Value in satoshis + }>; + + // Bitcoin transaction output + outputValue: uint64; // Output amount (total - fee) + + // Fee + feeRate: uint32; // Fee rate in sat/vB + totalFee: uint64; // Total fee in satoshis + + // Metadata + coordinationBlock: uint256; // Target Ethereum coordination block + provider: string; // "BOAR" | "STAKED" | "P2P" +} +``` + +**Example**: +```json +{ + "sourceWalletPKH": "0x1234567890abcdef1234567890abcdef12345678", + "targetWalletPKH": "0xffb804c2de78576ad011f68a7df63d739b8c8155", + "utxos": [ + { + "txHash": "0xabc123...", + "outputIndex": 0, + "value": 150000000 + }, + { + "txHash": "0xdef456...", + "outputIndex": 1, + "value": 100000000 + } + ], + "outputValue": 249995920, + "feeRate": 12, + "totalFee": 4080, + "coordinationBlock": 12345600, + "provider": "BOAR" +} +``` + +--- + +#### Step 1.5: Notify Operators + +**Action**: Send 48-hour advance notice to all operators. + +**Communication Channels**: + +1. **Email** (to all 18 operators): + ``` + Subject: Manual Sweep Required - Coordination Window 2025-11-29 14:00 UTC + + Body: + Dear Operators, + + Week 4 assessment shows the following wallets require manual sweeps: + - Wallet 0x1234...5678 (BOAR): 2.5 BTC + - Wallet 0x5678...abcd (STAKED): 3.1 BTC + - Wallet 0x9abc...efgh (P2P): 1.8 BTC + + Coordination Window: + - Ethereum Block: 12345600 (~2025-11-29 14:00 UTC) + - Duration: 100 blocks (~20 minutes) + - Required: 10 of 18 operators online + + Please confirm availability by replying to this email. + + Runbook: [link to detailed instructions] + Dashboard: [link to monitoring dashboard] + + Thank you, + Threshold Team + ``` + +2. **Slack/Discord** (in #operator-consolidation channel): + ``` + 🚨 Manual Sweep Coordination Required + + πŸ“… Date: 2025-11-29 14:00 UTC + ⛓️ Block: 12345600 + ⏱️ Duration: ~20 minutes + + Wallets to sweep: + β€’ BOAR: 2.5 BTC + β€’ STAKED: 3.1 BTC + β€’ P2P: 1.8 BTC + + Please ensure your node is online and responsive. + Reply with βœ… to confirm availability. + ``` + +3. **Calendar Invitation** (sent to all operators): + - Event: "Manual Sweep Coordination Window" + - Time: 2025-11-29 14:00 UTC (converted to operator timezones) + - Duration: 30 minutes (buffer beyond 20 min coordination window) + - Description: Link to runbook, dashboard, coordination details + +--- + +### Phase 2: Coordination Window Opens + +**Duration**: Every 900 blocks (~3 hours), window lasts 100 blocks (~20 minutes) + +#### Step 2.1: Determine Coordination Block + +**Action**: Calculate which Ethereum block opens the coordination window. + +**Technical Details**: +```python +coordination_frequency = 900 # blocks +current_block = eth_get_block_number() + +# Find next coordination block +if current_block % coordination_frequency == 0: + coordination_block = current_block +else: + coordination_block = current_block + (coordination_frequency - (current_block % coordination_frequency)) +``` + +**Example**: +``` +Current block: 12345678 +12345678 % 900 = 78 +Next coordination block: 12345678 + (900 - 78) = 12346500 +``` + +**Coordination Window**: +- **Opens**: Block 12346500 +- **Closes**: Block 12346600 (100 blocks later) +- **Duration**: 100 blocks Γ— 12 seconds = 1200 seconds = 20 minutes + +--- + +#### Step 2.2: Select Leader Operator + +**Action**: Deterministically select leader using RFC-12 algorithm. + +**Technical Details**: +```python +def select_leader(wallet_pkh: bytes, coordination_block: int, operators: List[Address]) -> Address: + # Get safe block hash (32 blocks before coordination block) + safe_block = coordination_block - 32 + safe_block_hash = eth_get_block_hash(safe_block) + + # Generate coordination seed + coordination_seed = sha256(wallet_pkh + safe_block_hash) + + # Initialize RNG with seed + rng = RNG(coordination_seed) + + # Shuffle operators and select first + shuffled_operators = rng.shuffle(operators) + leader = shuffled_operators[0] + + return leader +``` + +**Example**: +``` +Inputs: +- wallet_pkh: 0x1234567890abcdef1234567890abcdef12345678 +- coordination_block: 12346500 +- safe_block: 12346468 +- safe_block_hash: 0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +- operators: [0xOperator1, 0xOperator2, ..., 0xOperator18] + +Calculation: +- coordination_seed = sha256(0x1234...5678 + 0xabcd...7890) + = 0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba + +- rng = RNG(0x9876...dcba) +- shuffled = rng.shuffle([0xOp1, 0xOp2, ..., 0xOp18]) + = [0xOp7, 0xOp3, 0xOp12, ...] // deterministic shuffle + +Leader: 0xOp7 (first in shuffled list) +``` + +**Important**: All operators independently calculate this and arrive at the same leader. No centralized leader selection. + +--- + +### Phase 3: Leader Proposes MovingFunds + +**Duration**: First few minutes of coordination window + +#### Step 3.1: Leader Receives Proposal Parameters + +**Action**: Threshold team provides MovingFunds parameters to identified leader (via email or direct message). + +**Alternative**: Parameters could be posted publicly (encrypted or plaintext) for leader to retrieve. + +**Data Received**: Full `MovingFundsParameters` structure from Step 1.4. + +--- + +#### Step 3.2: Leader Validates Proposal Locally + +**Action**: Before broadcasting, leader performs local validation to ensure proposal is sound. + +**On-Chain Validation**: +```bash +# Call WalletProposalValidator contract +cast call $VALIDATOR_ADDRESS \ + "validateMovingFundsProposal(bytes20,bytes)" \ + $WALLET_PKH \ + $PROPOSAL_BYTES +``` + +**Expected Output**: `true` (proposal valid) + +**Off-Chain Validation**: +1. **Wallet State**: Confirm wallet is LIVE +2. **UTXOs Exist**: Verify UTXOs are unspent on Bitcoin blockchain +3. **Fee Reasonable**: Check fee rate is within acceptable range (not too high/low) +4. **Target Valid**: Confirm target wallet belongs to same provider +5. **Amount Correct**: Verify output amount = (sum of UTXOs) - fee + +**Example Checks**: +```python +def validate_proposal_offchain(params: MovingFundsParameters) -> bool: + # 1. Wallet is LIVE + wallet_state = wallet_registry.getWalletState(params.sourceWalletPKH) + if wallet_state != WalletState.LIVE: + return False + + # 2. UTXOs exist and unspent + for utxo in params.utxos: + if not bitcoin_is_unspent(utxo.txHash, utxo.outputIndex): + return False + + # 3. Fee reasonable (between 5 and 100 sat/vB) + if params.feeRate < 5 or params.feeRate > 100: + return False + + # 4. Target wallet is same provider + if not same_provider(params.sourceWalletPKH, params.targetWalletPKH): + return False + + # 5. Amount calculation correct + total_input = sum(utxo.value for utxo in params.utxos) + if params.outputValue != total_input - params.totalFee: + return False + + return True +``` + +--- + +#### Step 3.3: Leader Constructs CoordinationMessage + +**Action**: Package proposal into RFC-12 CoordinationMessage structure. + +**Data Structure**: +```typescript +interface CoordinationMessage { + memberId: number; // First signer controlled by leader + coordinationBlock: uint256; // Coordination block number + walletPublicKeyHash: bytes20; // Wallet being coordinated (source) + proposal: bytes; // Serialized MovingFunds proposal +} +``` + +**Proposal Serialization**: +``` +Proposal Bytes (MovingFunds): +- Target wallet PKH (20 bytes) +- Number of UTXOs (1 byte) +- For each UTXO: + - Transaction hash (32 bytes) + - Output index (4 bytes) + - Value (8 bytes) +- Output value (8 bytes) +- Fee rate (4 bytes) +``` + +**Example**: +```json +{ + "memberId": 42, + "coordinationBlock": 12346500, + "walletPublicKeyHash": "0x1234567890abcdef1234567890abcdef12345678", + "proposal": "0xffb804c2de78576ad011f68a7df63d739b8c8155020000000000......" +} +``` + +--- + +#### Step 3.4: Leader Signs and Broadcasts CoordinationMessage + +**Action**: Sign message with leader's private key and broadcast to all operators. + +**Signature**: +```python +message_hash = keccak256(encode(coordination_message)) +signature = ecdsa_sign(leader_private_key, message_hash) +``` + +**Broadcast Mechanism**: +- **Peer-to-peer network**: Direct connections between operator nodes +- **Gossip protocol**: Message propagates to all 18 operators within seconds +- **Backup channels**: Discord/Slack notification (optional) + +**Message Format**: +``` +CoordinationMessage + Signature: +{ + "message": { ... }, + "signature": { + "r": "0x1234...", + "s": "0x5678...", + "v": 27 + }, + "timestamp": 1701193200, + "leader": "0xOperator7" +} +``` + +--- + +### Phase 4: Followers Validate and Sign + +**Duration**: Remaining coordination window (~15 minutes after leader broadcasts) + +#### Step 4.1: Followers Receive CoordinationMessage + +**Action**: All 17 follower operators receive message via peer-to-peer network. + +**Reception Check**: +```python +def on_coordination_message_received(msg: CoordinationMessage, sig: Signature): + logger.info(f"Received coordination message from {msg.sender}") + logger.info(f"Block: {msg.coordinationBlock}, Wallet: {msg.walletPublicKeyHash}") + + # Proceed to validation + validate_and_sign(msg, sig) +``` + +--- + +#### Step 4.2: Followers Validate Leader Identity + +**Action**: Verify message was signed by expected leader. + +**Technical Details**: +```python +def validate_leader_identity(msg: CoordinationMessage, sig: Signature) -> bool: + # 1. Recover signer from signature + message_hash = keccak256(encode(msg)) + recovered_address = ecrecover(message_hash, sig) + + # 2. Calculate expected leader + expected_leader = select_leader(msg.walletPublicKeyHash, msg.coordinationBlock, all_operators) + + # 3. Verify match + if recovered_address != expected_leader: + logger.error(f"Invalid leader: got {recovered_address}, expected {expected_leader}") + return False + + return True +``` + +**If Validation Fails**: Follower ignores message (does not participate in signing). + +--- + +#### Step 4.3: Followers Validate Timing + +**Action**: Confirm message received within acceptable window. + +**Technical Details**: +```python +def validate_timing(msg: CoordinationMessage) -> bool: + current_block = eth_get_block_number() + + # Coordination window: [coordination_block, coordination_block + 100] + window_start = msg.coordinationBlock + window_end = msg.coordinationBlock + 100 + + # Message must arrive within 80% of window + window_80_percent = msg.coordinationBlock + 80 + + # Check 1: Current block is within window + if current_block < window_start or current_block > window_end: + logger.error(f"Outside coordination window: current={current_block}, window=[{window_start}, {window_end}]") + return False + + # Check 2: Not too late (within 80% of window) + if current_block > window_80_percent: + logger.warning(f"Message arrived late: current={current_block}, deadline={window_80_percent}") + return False + + return True +``` + +**If Validation Fails**: Follower ignores message (too late or outside window). + +--- + +#### Step 4.4: Followers Validate MovingFunds Proposal (On-Chain) + +**Action**: Call Ethereum smart contract to validate proposal parameters. + +**Technical Details**: +```solidity +// WalletProposalValidator.sol +function validateMovingFundsProposal( + bytes20 walletPubKeyHash, + bytes calldata movingFundsProposal +) external view returns (bool); +``` + +**Validation Logic** (inside contract): +1. Wallet is LIVE (not closed/terminated/locked) +2. Wallet has no pending MovingFunds action +3. Target wallet exists and is LIVE +4. Target wallet is controlled by same operators (or authorized subset) +5. UTXOs referenced in proposal match wallet's main UTXO +6. Fee calculation is correct + +**Operator Call**: +```bash +cast call $VALIDATOR_ADDRESS \ + "validateMovingFundsProposal(bytes20,bytes)" \ + $WALLET_PKH \ + $PROPOSAL_BYTES +``` + +**Expected Output**: `0x0000...0001` (true) + +**If Validation Fails**: Follower rejects proposal and does NOT sign. + +--- + +#### Step 4.5: Followers Validate MovingFunds Proposal (Off-Chain) + +**Action**: Perform Bitcoin-side validations that can't be done on Ethereum. + +**Checks**: +1. **UTXOs Exist and Unspent**: + ```bash + bitcoin-cli gettxout $TXID $VOUT + # Should return UTXO data (not null = unspent) + ``` + +2. **UTXO Values Match**: + - Query Bitcoin blockchain for each UTXO value + - Verify values match proposal parameters + +3. **Target Wallet is Bitcoin-Valid**: + - Derive Bitcoin address from target wallet PKH + - Verify it's a valid Bitcoin address format + +4. **Fee is Reasonable**: + - Check fee rate is within 5-100 sat/vB range + - Compare to current mempool recommendations + - Verify fee isn't excessive (no value extraction) + +5. **No Conflicting Transactions**: + - Check Bitcoin mempool for any pending transactions spending same UTXOs + - If found, reject proposal (double-spend attempt) + +**Example**: +```python +def validate_proposal_offchain_follower(proposal: MovingFundsProposal) -> bool: + for utxo in proposal.utxos: + # Check UTXO exists and unspent + utxo_data = bitcoin_get_txout(utxo.txHash, utxo.outputIndex) + if utxo_data is None: + logger.error(f"UTXO {utxo.txHash}:{utxo.outputIndex} is spent or doesn't exist") + return False + + # Verify value matches + if utxo_data.value != utxo.value: + logger.error(f"UTXO value mismatch: expected {utxo.value}, got {utxo_data.value}") + return False + + # Check fee reasonableness + if proposal.feeRate < 5 or proposal.feeRate > 100: + logger.error(f"Fee rate unreasonable: {proposal.feeRate} sat/vB") + return False + + return True +``` + +**If Validation Fails**: Follower rejects proposal and does NOT sign. + +--- + +#### Step 4.6: Followers Participate in Threshold Signing + +**Action**: If all validations pass, follower generates a partial signature share. + +**Threshold ECDSA Protocol**: + +The tBTC protocol uses **GG20** threshold ECDSA scheme (or similar): + +1. **Key Share**: Each operator holds a share of the wallet's private key (generated during DKG) +2. **Signing Ceremony**: Operators collaborate to produce a valid ECDSA signature WITHOUT reconstructing the full private key +3. **Threshold**: Need 51 out of 100 shares to produce valid signature + +**Signing Process** (simplified): + +```python +def participate_in_threshold_signing(proposal: MovingFundsProposal, wallet_pkh: bytes20): + # 1. Construct Bitcoin transaction to be signed + bitcoin_tx = construct_moving_funds_transaction(proposal) + + # 2. Generate signature hash (what we're signing) + sighash = bitcoin_tx.signature_hash(input_index=0, sighash_type=SIGHASH_ALL) + + # 3. Generate partial signature using key share + my_key_share = get_wallet_key_share(wallet_pkh) + partial_signature = gg20_sign_share(my_key_share, sighash) + + # 4. Broadcast partial signature to leader + send_to_leader(partial_signature) +``` + +**Partial Signature Structure**: +```typescript +interface PartialSignature { + operatorId: number; // Which operator produced this share + signatureShare: bytes; // The actual signature share + commitment: bytes; // Cryptographic commitment for verification +} +``` + +**Transmission**: Follower sends partial signature directly to leader (or broadcasts to all operators). + +--- + +### Phase 5: Leader Aggregates Signatures + +**Duration**: 2-5 minutes after receiving 51+ signature shares + +#### Step 5.1: Leader Collects Partial Signatures + +**Action**: Wait until at least 51 signature shares received. + +**Technical Details**: +```python +def collect_signatures(timeout_blocks: int = 80) -> List[PartialSignature]: + signatures = [] + start_block = eth_get_block_number() + + while len(signatures) < 51: + # Check timeout + current_block = eth_get_block_number() + if current_block - start_block > timeout_blocks: + logger.error(f"Timeout: only received {len(signatures)} signatures") + return None + + # Receive signature shares from followers + new_sig = receive_signature_share() + if new_sig: + # Verify share is valid (cryptographic check) + if verify_signature_share(new_sig): + signatures.append(new_sig) + logger.info(f"Received signature {len(signatures)}/51") + + return signatures +``` + +**Minimum Required**: 51 shares (threshold) +**Typical Expected**: 14-16 shares (with 18 operators, ~80-90% participation) + +**If Insufficient Signatures**: Coordination fails, retry in next coordination window (3 hours later). + +--- + +#### Step 5.2: Leader Aggregates into Complete Signature + +**Action**: Combine 51+ signature shares into single valid ECDSA signature. + +**Technical Details**: +```python +def aggregate_signatures(shares: List[PartialSignature]) -> ECDSASignature: + # GG20 signature aggregation + # This is cryptographic magic - multiple shares combine into one signature + aggregated_signature = gg20_aggregate(shares) + + # Verify aggregated signature is valid + wallet_public_key = get_wallet_public_key(wallet_pkh) + if not ecdsa_verify(wallet_public_key, sighash, aggregated_signature): + raise Exception("Aggregated signature invalid!") + + return aggregated_signature +``` + +**Output**: A single, standard ECDSA signature `(r, s)` that can be used in Bitcoin transaction. + +**Verification**: Leader verifies signature is valid before proceeding (safety check). + +--- + +#### Step 5.3: Leader Constructs Complete Bitcoin Transaction + +**Action**: Build final Bitcoin transaction with aggregated signature. + +**Bitcoin Transaction Structure**: +``` +Transaction { + version: 2 + + inputs: [ + { + previous_output: { + txid: "abc123...", + vout: 0 + }, + script_sig: , // The aggregated signature goes here + sequence: 0xFFFFFFFF + }, + { + previous_output: { + txid: "def456...", + vout: 1 + }, + script_sig: , + sequence: 0xFFFFFFFF + } + ] + + outputs: [ + { + value: 249995920, // 2.4999592 BTC (after fee deduction) + script_pubkey: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + } + ] + + locktime: 0 +} +``` + +**Script Signature**: +``` + = + = (derived from wallet PKH) +``` + +**Serialization**: +```python +def construct_bitcoin_transaction(proposal: MovingFundsProposal, signature: ECDSASignature) -> bytes: + tx = BitcoinTransaction() + tx.version = 2 + + # Add inputs + for utxo in proposal.utxos: + input = TxInput( + previous_output=OutPoint(utxo.txHash, utxo.outputIndex), + script_sig=build_script_sig(signature, wallet_public_key), + sequence=0xFFFFFFFF + ) + tx.inputs.append(input) + + # Add output + output = TxOutput( + value=proposal.outputValue, + script_pubkey=build_p2pkh_script(proposal.targetWalletPKH) + ) + tx.outputs.append(output) + + tx.locktime = 0 + + return tx.serialize() +``` + +--- + +#### Step 5.4: Leader Broadcasts Transaction to Bitcoin Network + +**Action**: Send signed transaction to Bitcoin network via Electrum servers. + +**Technical Details**: +```bash +# Broadcast via Electrum +electrum-client broadcast $SIGNED_TX_HEX + +# Or via Bitcoin Core +bitcoin-cli sendrawtransaction $SIGNED_TX_HEX +``` + +**Response**: +```json +{ + "result": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "error": null +} +``` + +Where `result` is the **transaction ID (txid)** on Bitcoin blockchain. + +**Propagation**: Transaction propagates to Bitcoin nodes, enters mempool, waits for miners. + +**Leader Actions**: +1. Broadcast to multiple Electrum servers (redundancy) +2. Verify transaction appears in mempool: `bitcoin-cli getmempoolentry $TXID` +3. Announce txid to all operators (via coordination channel) +4. Monitor transaction for confirmations + +--- + +### Phase 6: Bitcoin Confirmations + +**Duration**: ~1 hour (6 confirmations Γ— 10 minutes average) + +#### Step 6.1: Wait for Transaction to be Mined + +**Action**: Monitor Bitcoin blockchain for transaction inclusion. + +**Technical Details**: +```bash +# Check if transaction is mined +bitcoin-cli gettransaction $TXID +``` + +**Output** (before mining): +```json +{ + "confirmations": 0, + "txid": "1234...", + "time": 1701193200, + "details": [...] +} +``` + +**Output** (after 1 confirmation): +```json +{ + "confirmations": 1, + "blockhash": "0000000000000000000abc123...", + "blockheight": 825000, + "time": 1701193800, + "details": [...] +} +``` + +**Monitoring**: +```python +def wait_for_confirmations(txid: str, required_confirmations: int = 6): + while True: + tx_data = bitcoin_get_transaction(txid) + confirmations = tx_data.get('confirmations', 0) + + logger.info(f"Transaction {txid}: {confirmations}/{required_confirmations} confirmations") + + if confirmations >= required_confirmations: + logger.info(f"Transaction confirmed! Block: {tx_data['blockhash']}") + return tx_data + + # Wait for next block (~10 minutes) + time.sleep(600) +``` + +**Progress Updates**: Leader posts updates to operator coordination channel: +- "Transaction broadcast: txid abc123..." +- "1/6 confirmations received" +- "6/6 confirmations received - ready for SPV proof" + +--- + +#### Step 6.2: Verify BTC Arrived at Target Wallet + +**Action**: Check active wallet received expected BTC amount. + +**Technical Details**: +```bash +# Query active wallet balance +bitcoin-cli getreceivedbyaddress $ACTIVE_WALLET_ADDRESS 0 + +# Or check specific UTXO +bitcoin-cli gettxout $TXID $VOUT +``` + +**Expected**: +```json +{ + "bestblock": "00000000000000000000abc123...", + "confirmations": 6, + "value": 2.49995920, // Matches proposal.outputValue + "scriptPubKey": { + "address": "bc1q...", // Active wallet address + "type": "witness_v0_keyhash" + } +} +``` + +**Verification**: +```python +def verify_btc_received(txid: str, active_wallet_address: str, expected_value: int): + # Get transaction output + tx_data = bitcoin_get_transaction(txid) + + # Find output to active wallet + for output in tx_data['vout']: + if output['scriptPubKey']['address'] == active_wallet_address: + actual_value = int(output['value'] * 100000000) # Convert BTC to sats + + if actual_value == expected_value: + logger.info(f"βœ… Verified: {actual_value} sats received at {active_wallet_address}") + return True + else: + logger.error(f"❌ Amount mismatch: expected {expected_value}, got {actual_value}") + return False + + logger.error(f"❌ Output to {active_wallet_address} not found in transaction") + return False +``` + +--- + +### Phase 7: SPV Proof Submission to Ethereum + +**Duration**: 10-30 minutes + +#### Step 7.1: Construct SPV Proof + +**Action**: Build proof that Bitcoin transaction occurred and was confirmed. + +**SPV (Simplified Payment Verification) Proof Components**: + +1. **Bitcoin Transaction**: The complete, confirmed transaction +2. **Merkle Proof**: Proof that transaction is in a Bitcoin block +3. **Bitcoin Block Headers**: Headers proving the block exists in Bitcoin blockchain + +**Technical Details**: + +```python +def construct_spv_proof(txid: str) -> SPVProof: + # 1. Get confirmed transaction + tx_data = bitcoin_get_transaction(txid) + raw_tx = tx_data['hex'] + block_hash = tx_data['blockhash'] + + # 2. Get block with merkle tree + block_data = bitcoin_get_block(block_hash, verbosity=2) + + # 3. Build merkle proof + merkle_proof = build_merkle_proof(block_data['tx'], txid) + + # 4. Get block header + block_header = bitcoin_get_block_header(block_hash) + + # 5. Get preceding block headers (for chain proof) + # Ethereum Bridge needs N headers to verify chain validity + preceding_headers = [] + current_hash = block_hash + for i in range(6): # 6 confirmations = 6 headers + header = bitcoin_get_block_header(current_hash) + preceding_headers.append(header) + current_hash = header['previousblockhash'] + + return SPVProof( + transaction=raw_tx, + merkle_proof=merkle_proof, + block_header=block_header, + preceding_headers=preceding_headers + ) +``` + +**Merkle Proof**: +A Merkle proof is a list of hashes that, when combined with the transaction hash, reconstructs the Merkle root in the Bitcoin block header. + +``` +Block Merkle Tree: + Root + / \ + H1 H2 + / \ / \ + H3 H4 H5 H6 + /\ /\ /\ /\ + T1 T2 T3 T4 T5 T6 + +To prove T3 is in block: +Merkle Proof: [T4, H1, H2] + +Verification: +hash(hash(T3, T4), H1) = H2_calc +hash(H2_calc, H2) = Root_calc +Root_calc == Block.merkleRoot? βœ… Proven +``` + +--- + +#### Step 7.2: Submit SPV Proof to Ethereum Bridge + +**Action**: Call Bridge contract function to submit proof. + +**Ethereum Contract Function**: +```solidity +// Bridge.sol +function submitMovingFundsProof( + bytes calldata bitcoinTx, + bytes calldata merkleProof, + bytes calldata blockHeaders, + uint256 txIndexInBlock +) external; +``` + +**Call Example**: +```bash +cast send $BRIDGE_ADDRESS \ + "submitMovingFundsProof(bytes,bytes,bytes,uint256)" \ + $BITCOIN_TX_HEX \ + $MERKLE_PROOF_HEX \ + $BLOCK_HEADERS_HEX \ + $TX_INDEX \ + --private-key $SUBMITTER_PRIVATE_KEY \ + --gas-limit 500000 +``` + +**Who Submits**: Any operator or Threshold team member can submit (whoever submits first). + +**Gas Cost**: ~300,000-500,000 gas (depends on proof size) +- At 30 gwei: 500,000 Γ— 30 = 15,000,000 gwei = 0.015 ETH β‰ˆ $30 +- At 100 gwei: 500,000 Γ— 100 = 50,000,000 gwei = 0.05 ETH β‰ˆ $100 + +--- + +#### Step 7.3: Bridge Verifies SPV Proof + +**Action**: Bridge contract executes verification logic (automatic, on-chain). + +**Verification Steps** (inside smart contract): + +1. **Parse Bitcoin Transaction**: + - Extract inputs, outputs, amounts + - Verify transaction structure is valid + +2. **Verify Merkle Proof**: + - Recompute Merkle root using tx and proof + - Compare to Merkle root in block header + +3. **Verify Block Headers**: + - Check proof-of-work on each header (hash < target) + - Verify headers form valid chain (each references previous) + - Confirm headers are in Bitcoin blockchain + +4. **Check Confirmations**: + - Count confirmations (N headers after transaction block) + - Require >= 6 confirmations + +5. **Validate MovingFunds Action**: + - Extract source wallet PKH from transaction + - Extract target wallet PKH from transaction output + - Confirm matches expected MovingFunds action + +**If Verification Passes**: +- Event emitted: `MovingFundsProofSubmitted(walletPKH, txid, amount)` +- Wallet state updated (main UTXO changed) +- Deprecated wallet marked as having completed MovingFunds + +**If Verification Fails**: +- Transaction reverts +- No state change +- Submitter lost gas fees +- Can resubmit correct proof + +--- + +#### Step 7.4: Ethereum State Update + +**Action**: Bridge contract updates wallet state on Ethereum. + +**State Changes**: + +1. **Deprecated Wallet**: + ```solidity + wallets[deprecatedWalletPKH].mainUtxo = EmptyUtxo; // Wallet now empty + wallets[deprecatedWalletPKH].state = WalletState.MovedFunds; + ``` + +2. **Active Wallet**: + ```solidity + wallets[activeWalletPKH].mainUtxo = NewUtxo( + txHash: movingFundsTxid, + outputIndex: 0, + value: outputValue + ); + // Wallet's main UTXO now points to new BTC + ``` + +3. **Events Emitted**: + ```solidity + emit MovingFundsCompleted( + deprecatedWalletPKH, + activeWalletPKH, + movingFundsTxid, + outputValue + ); + ``` + +--- + +### Phase 8: Verification and Cleanup + +**Duration**: 1-2 days + +#### Step 8.1: Verify Wallet Balance On-Chain + +**Action**: Confirm deprecated wallet shows 0 BTC on Ethereum. + +**Technical Details**: +```bash +# Query wallet state +cast call $WALLET_REGISTRY_ADDRESS \ + "getWalletMainUtxo(bytes20)" \ + $DEPRECATED_WALLET_PKH + +# Should return empty UTXO (0 value) +``` + +**Expected Output**: +``` +(bytes32,uint32,uint64) = ( + 0x0000000000000000000000000000000000000000000000000000000000000000, // No txHash + 0, // No vout + 0 // 0 value +) +``` + +--- + +#### Step 8.2: Verify Wallet Balance Off-Chain (Bitcoin) + +**Action**: Confirm deprecated wallet shows 0 BTC on Bitcoin blockchain. + +**Technical Details**: +```bash +# Check wallet balance on Bitcoin +bitcoin-cli getreceivedbyaddress $DEPRECATED_WALLET_ADDRESS 0 + +# Should return 0 (or minimal dust like 0.00000546 BTC = 546 sats) +``` + +**Dust Handling**: If minimal dust remains (<1000 sats), consider it acceptable (not economical to sweep). + +--- + +#### Step 8.3: Mark Operator Eligible for Removal + +**Action**: Update operator status in monitoring system. + +**Technical Details**: +```python +# Update operator status +operator_status[deprecated_operator_address] = { + 'wallet_balance': 0, + 'wallet_empty_since': current_timestamp(), + 'eligible_for_removal_at': current_timestamp() + 7_days, + 'status': 'AWAITING_REMOVAL' +} +``` + +**Safety Buffer**: Wait 1 week at 0 BTC before actually removing operator. + +**Rationale**: Ensures no pending transactions or edge cases. + +--- + +#### Step 8.4: Notify Operators of Success + +**Action**: Send confirmation to all operators. + +**Communication**: + +1. **Email** (to all operators): + ``` + Subject: Manual Sweep Completed - Wallet 0x1234...5678 + + βœ… Manual sweep successfully completed! + + Wallet: 0x1234567890abcdef1234567890abcdef12345678 + Bitcoin Transaction: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef + Amount Moved: 2.5 BTC + Target Wallet: 0xffb804c2de78576ad011f68a7df63d739b8c8155 + + Confirmations: 6+ + SPV Proof: Submitted and verified on Ethereum + + Deprecated wallet now at 0 BTC. + Operator 0x1234...5678 will be eligible for removal on 2025-12-06. + ``` + +2. **Slack/Discord** (in #operator-consolidation channel): + ``` + βœ… Manual Sweep Complete + + Wallet: 0x1234...5678 + Amount: 2.5 BTC β†’ 0xffb8...8155 + Bitcoin TX: 1234...cdef + + Next: 1-week safety buffer, then operator removal. + ``` + +--- + +## Data Structures and Parameters + +### MovingFundsProposal Structure + +```typescript +interface MovingFundsProposal { + // Source wallet (being drained) + walletPublicKeyHash: bytes20; + + // Target wallet (receiving BTC, same provider) + targetWalletPublicKeyHash: bytes20; + + // Bitcoin transaction inputs (UTXOs to spend) + utxos: Array<{ + txHash: bytes32; // Previous transaction hash + outputIndex: uint32; // Output index (vout) + value: uint64; // Value in satoshis + }>; + + // Bitcoin transaction output + outputValue: uint64; // Amount sent to target (sats) + + // Fee + feeRate: uint32; // Fee rate in sat/vB + totalFee: uint64; // Total fee in satoshis + + // Metadata + coordinationBlock: uint256; // Target Ethereum block for coordination + provider: string; // Provider organization name + estimatedGasCost: uint256; // Estimated Ethereum gas for SPV proof +} +``` + +--- + +### CoordinationMessage Structure + +```typescript +interface CoordinationMessage { + // Identity + memberId: number; // First signer controlled by leader + + // Timing + coordinationBlock: uint256; // Block number when window opens + + // Wallet being coordinated + walletPublicKeyHash: bytes20; // 20-byte wallet PKH + + // Proposal (serialized) + proposal: bytes; // MovingFunds proposal bytes + + // Signature (added by leader) + signature: { + r: bytes32; + s: bytes32; + v: uint8; + }; +} +``` + +--- + +### SPVProof Structure + +```typescript +interface SPVProof { + // Bitcoin transaction + bitcoinTransaction: bytes; // Raw Bitcoin transaction (hex) + + // Merkle proof + merkleProof: bytes; // Hashes proving tx in block + txIndexInBlock: uint256; // Position of tx in block + + // Block headers + confirmingBlockHeader: bytes; // Header of block containing tx (80 bytes) + precedingHeaders: bytes; // Previous block headers for chain proof + + // Metadata + blockHeight: uint256; // Bitcoin block height + confirmations: uint256; // Number of confirmations +} +``` + +--- + +### Operator Status Tracking + +```typescript +interface OperatorStatus { + // Identity + operatorAddress: address; // Ethereum address + provider: string; // "BOAR" | "STAKED" | "P2P" + + // Wallet info + walletPublicKeyHash: bytes20; // Associated wallet PKH + walletBitcoinAddress: string; // Bitcoin address (derived) + + // Balance tracking + initialBalance: uint64; // Balance at consolidation start (sats) + currentBalance: uint64; // Current balance (sats) + lastUpdated: uint256; // Timestamp of last balance update + + // Draining progress + naturalDrainedAmount: uint64; // BTC drained via redemptions (sats) + manualSweptAmount: uint64; // BTC moved via manual sweep (sats) + percentDrained: uint8; // 0-100 percent + + // Status + status: "ACTIVE" | "DEPRECATED" | "AWAITING_REMOVAL" | "REMOVED"; + walletEmptySince: uint256; // Timestamp when wallet reached 0 BTC + eligibleForRemovalAt: uint256; // Timestamp + 1 week safety buffer + + // Manual sweep tracking + manualSweepRequired: boolean; // True if >20% after Week 4 + manualSweepCompleted: boolean; // True if sweep executed + manualSweepTxid: bytes32; // Bitcoin txid of sweep (if executed) +} +``` + +--- + +## Bitcoin Transaction Details + +### Transaction Structure + +A typical MovingFunds Bitcoin transaction: + +``` +Transaction ID: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef + +Version: 2 +Inputs: 2 +Outputs: 1 +Locktime: 0 + +Size: 340 bytes (typical) +Weight: 880 weight units (if SegWit) +Fee: 4,080 sats (12 sat/vB) + +Input 1: + Previous Transaction: abc1234567890... + Output Index: 0 + ScriptSig: + Sequence: 0xFFFFFFFF + Value: 150,000,000 sats (1.5 BTC) + +Input 2: + Previous Transaction: def9876543210... + Output Index: 1 + ScriptSig: + Sequence: 0xFFFFFFFF + Value: 100,000,000 sats (1.0 BTC) + +Output 1: + Value: 249,995,920 sats (2.4999592 BTC) + ScriptPubKey: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + Address: bc1q... (active wallet) + +Total Input: 250,000,000 sats +Total Output: 249,995,920 sats +Fee: 4,080 sats (250,000,000 - 249,995,920) +``` + +--- + +### Script Types + +**Input Script (ScriptSig)**: +``` + + +Where: +- signature: 71-73 bytes (DER-encoded ECDSA signature) +- public_key: 33 bytes (compressed) or 65 bytes (uncompressed) +``` + +**Output Script (ScriptPubKey) - P2PKH**: +``` +OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + +Where: +- OP_DUP: 0x76 +- OP_HASH160: 0xa9 +- : 20 bytes (RIPEMD160(SHA256(pubkey))) +- OP_EQUALVERIFY: 0x88 +- OP_CHECKSIG: 0xac +``` + +**Output Script (ScriptPubKey) - P2WPKH (SegWit)**: +``` +0 + +Where: +- 0: OP_0 (witness version 0) +- : 20 bytes +``` + +--- + +### Fee Calculation + +**Formula**: +``` +Fee (sats) = Transaction Size (bytes) Γ— Fee Rate (sat/vB) +``` + +**Transaction Size Estimation**: +``` +Base Size: +- Version: 4 bytes +- Input Count: 1 byte +- Output Count: 1 byte +- Locktime: 4 bytes +- Total Base: 10 bytes + +Per Input (P2PKH): +- Previous TX Hash: 32 bytes +- Output Index: 4 bytes +- Script Length: 1 byte +- ScriptSig: ~107 bytes (signature ~72 + pubkey ~33 + opcodes) +- Sequence: 4 bytes +- Total Per Input: 148 bytes + +Per Output (P2PKH): +- Value: 8 bytes +- Script Length: 1 byte +- ScriptPubKey: 25 bytes +- Total Per Output: 34 bytes + +Example Transaction (2 inputs, 1 output): +10 + (2 Γ— 148) + (1 Γ— 34) = 10 + 296 + 34 = 340 bytes + +Fee at 12 sat/vB: +340 Γ— 12 = 4,080 sats = 0.0000408 BTC β‰ˆ $2 at $50k/BTC +``` + +--- + +### Transaction Verification + +**Verification by Bitcoin Nodes**: + +1. **Input Validation**: + - Previous outputs (UTXOs) exist and are unspent + - Signatures are valid (verify using public keys) + - Sum of input values β‰₯ sum of output values + fee + +2. **Script Execution**: + - Execute ScriptSig + ScriptPubKey + - Result must be TRUE (1 on stack) + +3. **Consensus Rules**: + - Transaction size < 100 KB + - No double-spends (inputs not already spent) + - Output values > 0 (no negative outputs) + +--- + +## Ethereum Smart Contract Interactions + +### Contracts Involved + +1. **Allowlist.sol**: Manages operator weights (already deployed) +2. **WalletRegistry.sol**: Tracks wallet state and UTXOs +3. **Bridge.sol**: Accepts SPV proofs and updates state +4. **WalletProposalValidator.sol**: Validates MovingFunds proposals + +--- + +### Key Contract Functions + +#### WalletProposalValidator.validateMovingFundsProposal() + +```solidity +function validateMovingFundsProposal( + bytes20 walletPubKeyHash, + bytes calldata movingFundsProposal +) external view returns (bool) { + // Decode proposal + (bytes20 targetWalletPKH, bytes memory utxos, uint64 outputValue) = + decodeMovingFundsProposal(movingFundsProposal); + + // Get source wallet + Wallet memory sourceWallet = walletRegistry.wallets(walletPubKeyHash); + + // Check 1: Wallet is LIVE + require(sourceWallet.state == WalletState.LIVE, "Wallet not live"); + + // Check 2: No pending MovingFunds + require(!sourceWallet.movingFundsPending, "MovingFunds pending"); + + // Check 3: Target wallet exists and is LIVE + Wallet memory targetWallet = walletRegistry.wallets(targetWalletPKH); + require(targetWallet.state == WalletState.LIVE, "Target not live"); + + // Check 4: UTXOs match wallet's main UTXO + require(utxosMatchMainUtxo(utxos, sourceWallet.mainUtxo), "UTXO mismatch"); + + // Check 5: Fee calculation correct + uint64 totalInput = calculateTotalInput(utxos); + uint64 expectedOutput = totalInput - calculateFee(utxos.length); + require(outputValue == expectedOutput, "Fee incorrect"); + + return true; +} +``` + +--- + +#### Bridge.submitMovingFundsProof() + +```solidity +function submitMovingFundsProof( + bytes calldata bitcoinTx, + bytes calldata merkleProof, + bytes calldata blockHeaders, + uint256 txIndexInBlock +) external { + // 1. Verify SPV proof + require(verifySPVProof(bitcoinTx, merkleProof, blockHeaders, txIndexInBlock), "Invalid SPV proof"); + + // 2. Parse Bitcoin transaction + (bytes20 sourceWalletPKH, bytes20 targetWalletPKH, uint64 outputValue) = + parseBitcoinTransaction(bitcoinTx); + + // 3. Verify MovingFunds action was authorized + require(walletRegistry.isMovingFundsAuthorized(sourceWalletPKH), "Not authorized"); + + // 4. Update source wallet state + walletRegistry.updateWallet(sourceWalletPKH, EmptyUtxo, WalletState.MovedFunds); + + // 5. Update target wallet state + bytes32 txid = sha256(sha256(bitcoinTx)); // Bitcoin double SHA256 + Utxo memory newUtxo = Utxo(txid, 0, outputValue); + walletRegistry.updateWallet(targetWalletPKH, newUtxo, WalletState.LIVE); + + // 6. Emit event + emit MovingFundsCompleted(sourceWalletPKH, targetWalletPKH, txid, outputValue); +} +``` + +--- + +#### SPV Proof Verification Logic + +```solidity +function verifySPVProof( + bytes calldata bitcoinTx, + bytes calldata merkleProof, + bytes calldata blockHeaders, + uint256 txIndexInBlock +) internal view returns (bool) { + // 1. Calculate transaction hash (Bitcoin uses double SHA256) + bytes32 txHash = sha256(sha256(bitcoinTx)); + + // 2. Verify Merkle proof + bytes32 computedRoot = computeMerkleRoot(txHash, merkleProof, txIndexInBlock); + + // 3. Extract Merkle root from block header + bytes32 blockMerkleRoot = extractMerkleRoot(blockHeaders[0:80]); + require(computedRoot == blockMerkleRoot, "Merkle root mismatch"); + + // 4. Verify block headers form valid chain + require(verifyBlockChain(blockHeaders), "Invalid block chain"); + + // 5. Verify proof-of-work on each block + for (uint i = 0; i < blockHeaders.length / 80; i++) { + bytes memory header = blockHeaders[i*80:(i+1)*80]; + require(verifyProofOfWork(header), "Invalid PoW"); + } + + // 6. Check confirmations (number of headers provided) + uint confirmations = blockHeaders.length / 80; + require(confirmations >= 6, "Insufficient confirmations"); + + return true; +} +``` + +--- + +## RFC-12 Coordination Protocol + +### Coordination Windows + +**Frequency**: Every 900 blocks (~3 hours) + +**Calculation**: +```python +if current_block % 900 == 0: + # Coordination window is open + coordination_block = current_block +else: + # Calculate next window + blocks_until_next = 900 - (current_block % 900) + coordination_block = current_block + blocks_until_next +``` + +**Example**: +``` +Current block: 12,345,678 +12,345,678 % 900 = 78 + +Next coordination window: +12,345,678 + (900 - 78) = 12,346,500 + +Window duration: Blocks 12,346,500 to 12,346,600 (100 blocks) +Time: ~20 minutes +``` + +--- + +### Leader Selection Algorithm + +**Deterministic Selection**: +All operators independently calculate the same leader using: + +```python +def select_leader(wallet_pkh, coordination_block, operators): + # 1. Get safe block (32 blocks before coordination window) + safe_block = coordination_block - 32 + safe_block_hash = eth_get_block_hash(safe_block) + + # 2. Generate seed + seed = sha256(wallet_pkh + safe_block_hash) + + # 3. Shuffle operators deterministically + rng = RNG(seed) + shuffled = rng.shuffle(operators) + + # 4. First operator is leader + return shuffled[0] +``` + +**Properties**: +- **Deterministic**: Same inputs always produce same leader +- **Unpredictable**: Cannot predict leader until safe block is mined +- **Fair**: Each operator has equal probability over time + +--- + +### Leader Responsibilities + +1. **Propose Action**: Construct and broadcast CoordinationMessage +2. **Collect Signatures**: Receive partial signatures from followers +3. **Aggregate**: Combine signatures into complete ECDSA signature +4. **Execute**: Broadcast Bitcoin transaction +5. **Report**: Announce transaction ID to all operators + +--- + +### Follower Responsibilities + +1. **Validate Leader**: Verify message sender is expected leader +2. **Validate Timing**: Confirm message within coordination window +3. **Validate Proposal**: Run on-chain and off-chain checks +4. **Sign**: Generate partial signature if valid +5. **Submit**: Send signature share to leader + +--- + +### Coordination Window Timeline + +``` +Block 12,346,500: Coordination window opens + ↓ +Block 12,346,502: Leader calculates (knows they are leader) + ↓ +Block 12,346,505: Leader broadcasts CoordinationMessage + ↓ +Block 12,346,510: Followers validate and start signing + ↓ +Block 12,346,520: Leader receives 51+ signature shares + ↓ +Block 12,346,525: Leader aggregates signatures + ↓ +Block 12,346,530: Leader constructs Bitcoin transaction + ↓ +Block 12,346,535: Leader broadcasts to Bitcoin network + ↓ +Block 12,346,580: Coordination window 80% complete (deadline) + ↓ +Block 12,346,600: Coordination window closes + +Success: Transaction broadcast by block 12,346,535 +``` + +--- + +### Fallback: If Leader Doesn't Act + +If leader fails to propose (offline, malfunction, censoring): + +**Retry in Next Window**: +- Wait for next coordination window (3 hours later) +- New leader selected deterministically +- Repeat process + +**Different Leader Each Time**: +- Each window has different safe block hash +- Different seed β†’ different shuffle β†’ different leader +- Probability that 3 consecutive leaders all fail: (1/18)Β³ = 0.017% + +--- + +## Failure Scenarios and Recovery + +### Scenario 1: Insufficient Operators Online (<51) + +**Problem**: Only 8 of 18 operators available during coordination window. + +**Detection**: +```python +if len(signature_shares) < 51: + logger.error(f"Threshold not met: only {len(signature_shares)} signatures") +``` + +**Recovery**: +1. Leader announces failure in coordination channel +2. Threshold team posts reminder to all operators +3. Schedule coordination attempt for next window (3 hours later) +4. Ensure more operators available (direct communication with providers) + +**Prevention**: +- Send 48-hour advance notice +- Confirm operator availability before window +- Schedule during business hours (not weekends/holidays) + +--- + +### Scenario 2: Leader Offline or Unresponsive + +**Problem**: Selected leader's node is offline or malfunctioning. + +**Detection**: +```python +# Followers detect no CoordinationMessage after 50 blocks +if current_block > coordination_block + 50 and not received_coordination_message(): + logger.warning("Leader appears offline or unresponsive") +``` + +**Recovery**: +1. Wait for coordination window to close (no action taken) +2. Next window opens in 3 hours β†’ new leader selected +3. New leader attempts coordination + +**Probability**: +- With 18 operators, 10+ must be online (56% threshold) +- If 80% of operators online (14/18), probability leader is offline: 22% +- Multiple windows ensure success: 99%+ probability within 3 attempts + +--- + +### Scenario 3: Invalid Proposal Parameters + +**Problem**: MovingFunds proposal contains errors (wrong UTXOs, incorrect fee, etc.). + +**Detection**: +```python +def validate_proposal_offchain(proposal): + if not verify_utxos_unspent(proposal.utxos): + logger.error("UTXOs are spent or don't exist") + return False + + if not verify_fee_reasonable(proposal.feeRate): + logger.error(f"Fee unreasonable: {proposal.feeRate} sat/vB") + return False + + return True +``` + +**Recovery**: +1. Followers reject proposal (do not sign) +2. Leader receives <51 signatures β†’ coordination fails +3. Threshold team corrects proposal parameters +4. Retry in next coordination window with fixed parameters + +**Prevention**: +- Double-check UTXO queries before constructing proposal +- Validate proposal locally before giving to leader +- Use automated tools to generate correct parameters + +--- + +### Scenario 4: Bitcoin Transaction Doesn't Confirm (Low Fee) + +**Problem**: Bitcoin transaction broadcast but stuck in mempool due to low fee. + +**Detection**: +```bash +# Transaction in mempool for >2 hours without confirmation +bitcoin-cli getmempoolentry $TXID +# Shows confirmations: 0, time: 2+ hours ago +``` + +**Recovery**: +1. **Option A - Wait**: If fee is reasonable, wait longer (could take days) +2. **Option B - Replace-By-Fee (RBF)**: Broadcast replacement transaction with higher fee + ```bash + # Construct new transaction with same inputs, higher fee + # Mark original transaction as RBF-enabled (sequence < 0xFFFFFFFE) + bitcoin-cli createrawtransaction '[...]' '{...}' 0 true + ``` +3. **Option C - Child-Pays-For-Parent (CPFP)**: Spend output with higher fee transaction + +**Prevention**: +- Check current mempool congestion before coordination +- Use recommended fee rates (not lowest) +- Enable RBF flag on transaction (allows replacement) + +--- + +### Scenario 5: SPV Proof Submission Fails + +**Problem**: SPV proof transaction reverts on Ethereum (invalid proof or gas limit). + +**Detection**: +```bash +# Ethereum transaction reverts +cast send ... +# Error: transaction reverted +``` + +**Recovery**: +1. **Debug**: Check revert reason + ```bash + cast call $BRIDGE_ADDRESS "getRevertReason(bytes32)" $TX_HASH + ``` +2. **Fix Issue**: + - If invalid Merkle proof: Reconstruct proof correctly + - If insufficient gas: Increase gas limit + - If insufficient confirmations: Wait for more blocks +3. **Resubmit**: Call `submitMovingFundsProof` again + +**Note**: Bitcoin transaction is already confirmed, so BTC has moved. SPV proof just tells Ethereum about it. + +--- + +### Scenario 6: Wallet Has Dust Remaining + +**Problem**: After sweep, wallet has minimal dust (e.g., 546 sats). + +**Reason**: Bitcoin has "dust limit" - outputs below ~546 sats are non-standard and won't propagate. + +**Decision**: +- **Accept dust**: Not economical to sweep (fee > dust value) +- **Mark as complete**: Wallet effectively empty + +**On-Chain State**: +```solidity +// Wallet marked as MovedFunds even with dust +wallets[walletPKH].state = WalletState.MovedFunds; +``` + +**Operator Removal**: Proceed with operator removal (dust is negligible). + +--- + +### Scenario 7: Wrong Target Wallet in Proposal + +**Problem**: Proposal accidentally specifies wrong target wallet (e.g., different provider). + +**Detection**: +```python +def validate_same_provider(source_pkh, target_pkh): + source_provider = get_wallet_provider(source_pkh) + target_provider = get_wallet_provider(target_pkh) + + if source_provider != target_provider: + logger.error(f"Provider mismatch: {source_provider} != {target_provider}") + return False + + return True +``` + +**Recovery**: +- **Before Signing**: Followers detect error, reject proposal, coordination fails +- **After Signing**: If BTC already moved to wrong wallet, requires manual coordination between providers to return BTC + +**Prevention**: +- Strict validation in proposal construction +- Followers MUST check provider match before signing +- Use provider-specific proposal templates + +--- + +## Cost Analysis + +### Bitcoin Transaction Fees + +**Variable Factors**: +- Transaction size (depends on # of UTXOs) +- Mempool congestion (fee rate in sat/vB) +- Urgency (faster confirmation = higher fee) + +**Typical Costs**: + +| Scenario | Size | Fee Rate | Total Fee | USD Cost | +|----------|------|----------|-----------|----------| +| Small (1 UTXO) | 192 bytes | 10 sat/vB | 1,920 sats | $0.96 | +| Medium (2 UTXOs) | 340 bytes | 12 sat/vB | 4,080 sats | $2.04 | +| Large (5 UTXOs) | 784 bytes | 15 sat/vB | 11,760 sats | $5.88 | +| Urgent (high fee) | 340 bytes | 50 sat/vB | 17,000 sats | $8.50 | + +*Assuming BTC = $50,000* + +**Worst Case** (extreme mempool congestion): +- Fee rate: 100 sat/vB +- Transaction size: 784 bytes (5 UTXOs) +- Total fee: 78,400 sats = $39.20 + +--- + +### Ethereum Gas Fees + +**SPV Proof Submission**: + +| Gas Price | Gas Used | Total Cost (ETH) | USD Cost | +|-----------|----------|------------------|----------| +| 20 gwei | 400,000 | 0.008 ETH | $16 | +| 50 gwei | 400,000 | 0.020 ETH | $40 | +| 100 gwei | 400,000 | 0.040 ETH | $80 | +| 200 gwei | 500,000 | 0.100 ETH | $200 | + +*Assuming ETH = $2,000* + +**Gas Breakdown**: +- Merkle proof verification: ~100,000 gas +- Block header verification: ~150,000 gas +- State updates (storage writes): ~100,000 gas +- Event emissions: ~50,000 gas + +--- + +### Total Cost Per Manual Sweep + +**Typical Sweep**: +- Bitcoin fee: $2-5 +- Ethereum gas: $20-50 +- **Total: $22-55 per wallet** + +**Worst Case** (high congestion): +- Bitcoin fee: $10-40 +- Ethereum gas: $80-200 +- **Total: $90-240 per wallet** + +--- + +### Total Project Cost + +**If All 15 Wallets Require Manual Sweeps**: + +| Scenario | Cost per Wallet | Total (15 wallets) | +|----------|-----------------|-------------------| +| Optimistic | $25 | $375 | +| Typical | $50 | $750 | +| Worst Case | $150 | $2,250 | + +**Expected Reality** (hybrid approach): +- 50-70% drain naturally (free) +- 30-50% require manual sweeps (7-8 wallets) +- **Estimated Cost: $350-600** + +--- + +### Who Pays? + +**DAO Treasury**: All costs paid from Threshold DAO treasury. +- Governance approval obtained in advance +- Budget allocated from cost savings + +**Individual Operators**: No out-of-pocket costs. + +--- + +## Timing and Scheduling + +### Coordination Window Schedule + +**Frequency**: Every 900 blocks = ~3 hours + +**Daily Windows**: +``` +00:00 UTC - Block 12,345,000 +03:00 UTC - Block 12,345,900 +06:00 UTC - Block 12,346,800 +09:00 UTC - Block 12,347,700 +12:00 UTC - Block 12,348,600 +15:00 UTC - Block 12,349,500 +18:00 UTC - Block 12,350,400 +21:00 UTC - Block 12,351,300 +``` + +**Preferred Timing** (for operator availability): +- **09:00-18:00 UTC**: Business hours in Europe and Asia +- **15:00-21:00 UTC**: Business hours in Americas +- **Avoid**: Weekends, holidays, midnight hours + +--- + +### Bitcoin Confirmation Time + +**Expected**: 6 confirmations Γ— 10 minutes = ~60 minutes + +**Actual Range**: +- **Fast**: 30-45 minutes (lucky with fast blocks) +- **Typical**: 60-90 minutes +- **Slow**: 2-3 hours (if unlucky with block times) + +**Factors**: +- Bitcoin's variable block time (target 10 min, actual 8-12 min) +- Mempool congestion (lower fee = longer wait) +- Network hashrate fluctuations + +--- + +### Complete Sweep Timeline + +**End-to-End Duration**: +``` +T+0 hours: Coordination window opens +T+0.5 hours: Leader broadcasts transaction to Bitcoin +T+1.5 hours: Bitcoin transaction gets 6 confirmations +T+2 hours: SPV proof constructed +T+2.5 hours: SPV proof submitted to Ethereum +T+3 hours: Verification complete + +Total: ~3 hours from coordination start to completion +``` + +**Worst Case** (failures and retries): +``` +T+0 hours: First coordination attempt (failed - <51 operators) +T+3 hours: Second coordination attempt (failed - invalid proposal) +T+6 hours: Third coordination attempt (success) +T+7.5 hours: Bitcoin transaction confirmed +T+8 hours: SPV proof submitted +T+9 hours: Complete + +Total: ~9 hours (3 coordination attempts) +``` + +--- + +### Project Timeline + +**Week 4 Assessment** (2025-12-01): +- Identify 7-8 wallets requiring manual sweeps + +**Week 5 Execution** (2025-12-02 to 2025-12-06): +- Day 1: Sweep wallets 1-3 (BOAR) +- Day 2: Sweep wallets 4-6 (STAKED) +- Day 3: Sweep wallets 7-8 (P2P) +- Day 4-5: Verify all sweeps, handle retries if needed + +**Week 6+ Operator Removal** (2025-12-12 onwards): +- 1 week safety buffer after 0 BTC +- Progressive removal in batches + +--- + +## Security Considerations + +### Threshold Cryptography Security + +**Private Key Never Reconstructed**: +- Each operator holds a secret share +- 51 shares can produce valid signature +- Full private key NEVER exists in memory +- Even leader cannot extract private key from signature shares + +**Properties**: +- **Threshold Security**: Attacker needs 51+ shares to compromise wallet +- **Robustness**: System works even if 49 operators offline +- **Non-Interactivity**: Signing requires coordination but is non-interactive cryptographically + +--- + +### SPV Proof Security + +**What SPV Proves**: +- Transaction exists in a Bitcoin block +- Block has valid proof-of-work +- Block is part of Bitcoin's longest chain +- Transaction has N confirmations + +**What SPV Doesn't Prove**: +- Transaction is economically final (6 confirmations is convention) +- No future reorganization will occur (low probability after 6 confirms) + +**Bridge Contract Validation**: +- Requires 6 confirmations minimum +- Verifies proof-of-work on each block +- Checks Merkle proof correctness +- Validates block chain integrity + +**Attack Resistance**: +- **51% Attack**: Attacker would need >50% of Bitcoin hashrate to create fake blocks +- **Merkle Tree Attack**: Cryptographically impossible to fake without collision +- **Reorganization Attack**: Probability of 6-block reorg: ~0.0001% + +--- + +### Proposal Validation Security + +**Multiple Layers**: +1. **Leader Local Validation**: Before broadcasting +2. **On-Chain Validation**: WalletProposalValidator contract +3. **Follower Off-Chain Validation**: Each operator independently checks +4. **Ethereum Bridge Validation**: Final SPV proof verification + +**Defense in Depth**: Malicious proposal must pass 4 independent checks to succeed. + +--- + +### Operator Security + +**Private Key Protection**: +- Operators store key shares in secure hardware (HSM, encrypted storage) +- Key shares never transmitted over network +- Signatures generated locally, only shares transmitted + +**Node Security**: +- Operators run nodes on secure infrastructure +- Regular security updates and monitoring +- Access controls and firewalls + +**Communication Security**: +- Peer-to-peer network uses TLS encryption +- CoordinationMessages are signed (prevents spoofing) +- Leader identity verified by signature + +--- + +### Transaction Irreversibility + +**Bitcoin Transaction**: Once confirmed (6+ blocks), effectively irreversible. +- Reorganizations >6 blocks are extremely rare +- If sweep goes to wrong wallet, requires manual return (no automatic rollback) + +**Prevention**: +- Multiple layers of validation before signing +- Operators must verify target wallet is correct +- Use provider-specific proposal templates (reduces human error) + +--- + +## Monitoring and Verification + +### Real-Time Monitoring + +**Grafana Dashboard Metrics**: +1. **Coordination Status**: + - Current coordination block + - Time until next window + - Active operators count + +2. **Manual Sweep Progress**: + - Wallets requiring sweeps (list) + - Coordination attempts (success/failure) + - Bitcoin transactions in progress (txids) + - SPV proofs submitted (pending/confirmed) + +3. **Wallet Balances**: + - Deprecated wallet balances (BTC) + - Active wallet balances (BTC) + - Total BTC moved today/week + +4. **Operator Participation**: + - Operators online/offline + - Signature participation rate + - Leader selection history + +--- + +### Alerts + +**Critical Alerts** (immediate action required): + +1. **Coordination Failure**: + - Trigger: <51 signatures after 80 blocks + - Action: Contact providers, ensure operators online for next window + +2. **Bitcoin Transaction Stuck**: + - Trigger: Transaction in mempool >2 hours without confirmation + - Action: Consider RBF (Replace-By-Fee) or higher fee + +3. **SPV Proof Revert**: + - Trigger: SPV proof submission fails on Ethereum + - Action: Debug revert reason, resubmit corrected proof + +**Warning Alerts** (monitor closely): + +1. **Low Operator Count**: + - Trigger: <12 operators online (below comfortable margin) + - Action: Notify providers to bring more operators online + +2. **High Bitcoin Fees**: + - Trigger: Mempool fee >50 sat/vB + - Action: Consider delaying sweep to lower-fee period (if not urgent) + +3. **Ethereum Gas Spike**: + - Trigger: Gas price >100 gwei + - Action: Delay SPV proof submission until gas drops (if not urgent) + +--- + +### Post-Sweep Verification Checklist + +After each manual sweep, verify: + +- [ ] Bitcoin transaction has 6+ confirmations +- [ ] BTC arrived at correct active wallet (address matches provider) +- [ ] Amount matches expected (total input - fee = output) +- [ ] SPV proof submitted and verified on Ethereum +- [ ] Deprecated wallet on-chain state updated (mainUtxo = empty) +- [ ] Active wallet on-chain state updated (mainUtxo = new tx) +- [ ] Operator status updated (AWAITING_REMOVAL) +- [ ] Dashboard reflects 0 BTC balance +- [ ] Success notification sent to all operators +- [ ] Cost recorded (Bitcoin fee + Ethereum gas) + +--- + +## Operator Instructions + +### For All Operators + +**Before Coordination Window**: + +1. **Verify Node Health**: + ```bash + # Check node is running + systemctl status tbtc-node + + # Check sync status + curl http://localhost:8080/health + # Should return: {"status": "healthy", "synced": true} + ``` + +2. **Confirm Ethereum Sync**: + ```bash + # Check block height matches current + curl http://localhost:8545 -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' + ``` + +3. **Verify Bitcoin Access**: + ```bash + # Test Electrum connection + electrum-client getinfo + ``` + +4. **Acknowledge Coordination Notice**: + - Reply to email/Slack with βœ… confirming availability + - Ensure you'll be monitoring during coordination window + +--- + +**During Coordination Window** (20 minutes): + +1. **Monitor Coordination Channel**: + - Watch for leader announcement + - Check for CoordinationMessage broadcast + +2. **Validate Proposal** (if you're a follower): + - Wait for CoordinationMessage from leader + - Run local validation (on-chain + off-chain) + - If valid, participate in signing (automatic by node) + - If invalid, log reason and DO NOT sign + +3. **Execute Proposal** (if you're the leader): + - Receive proposal parameters from Threshold team + - Validate locally + - Construct and broadcast CoordinationMessage + - Collect signature shares (wait for 51+) + - Aggregate signatures + - Construct and broadcast Bitcoin transaction + - Announce txid to all operators + +4. **Monitor Progress**: + - Watch for Bitcoin transaction ID announcement + - Verify transaction appears in Bitcoin mempool + - Monitor confirmations + +--- + +**After Coordination Window**: + +1. **Verify Bitcoin Transaction**: + ```bash + # Check transaction status + bitcoin-cli gettransaction $TXID + + # Verify it reached target wallet + bitcoin-cli getreceivedbyaddress $TARGET_WALLET_ADDRESS + ``` + +2. **Wait for SPV Proof**: + - Monitor Ethereum for SPV proof submission + - Verify on-chain state updated correctly + +3. **Update Records**: + - Log sweep details (txid, amount, timestamp) + - Update local operator status tracking + +4. **Report Any Issues**: + - If transaction didn't confirm: Report to coordination channel + - If SPV proof failed: Share error details with team + - If discrepancies in amounts: Escalate immediately + +--- + +### For Deprecated Operators (Wallets Being Drained) + +**Additional Responsibilities**: + +1. **Monitor Your Wallet Balance**: + ```bash + # Check current balance + bitcoin-cli getreceivedbyaddress $YOUR_DEPRECATED_WALLET_ADDRESS 0 + ``` + +2. **Participate in Coordination**: + - Your node MUST be online during sweep + - You will participate in threshold signing (part of 51/100) + +3. **Verify Wallet Emptied**: + ```bash + # After sweep, confirm 0 BTC (or minimal dust) + bitcoin-cli getreceivedbyaddress $YOUR_DEPRECATED_WALLET_ADDRESS 0 + # Should return: 0.00000000 or ~0.00000546 (dust) + ``` + +4. **Wait for Decommissioning Approval**: + - DO NOT shut down node immediately after 0 BTC + - Wait for 1-week safety buffer + - Wait for explicit email confirmation from Threshold team + - Only then proceed to decommission node + +--- + +### For Active Operators (Receiving BTC) + +**Additional Responsibilities**: + +1. **Monitor Your Wallet Balance**: + ```bash + # Check balance increase after sweep + bitcoin-cli getreceivedbyaddress $YOUR_ACTIVE_WALLET_ADDRESS 0 + ``` + +2. **Verify Amount Received**: + - Compare received amount to expected (should match proposal parameters) + - Report any discrepancies immediately + +3. **Continue Normal Operations**: + - Your wallet remains active after consolidation + - Continue participating in DKG, redemptions, etc. + - Monitor for increased leader probability (~33% after consolidation) + +--- + +## Emergency Contacts + +### Threshold Team + +- **Engineering Lead**: [email - TBD] +- **DevOps Coordinator**: [email - TBD] +- **Emergency Hotline**: [phone - TBD] + +### Provider Liaisons + +- **BOAR**: [contact - TBD] +- **STAKED**: [contact - TBD] +- **P2P**: [contact - TBD] + +### Communication Channels + +- **Slack**: #operator-consolidation +- **Discord**: #tbtc-operators +- **Email**: operators@threshold.network + +--- + +## Conclusion + +Manual sweeps are a **fallback mechanism** designed to ensure the consolidation completes on time if natural draining is insufficient. The process leverages: + +βœ… **Existing tBTC infrastructure** (MovingFunds mechanism) +βœ… **RFC-12 decentralized coordination** (no central authority) +βœ… **Threshold cryptography** (secure, robust signing) +βœ… **SPV proofs** (trustless Ethereum ↔ Bitcoin bridge) + +**Expected Reality**: +- 50-70% of wallets drain naturally (no manual intervention) +- 30-50% require manual sweeps (7-8 wallets) +- Total project cost: $350-600 + +**Timeline**: +- Week 4 assessment determines which wallets need sweeps +- Week 5 execution (3-5 days to complete all sweeps) +- Week 6+ operator removal (after 1-week safety buffer) + +**Success Criteria**: +- All 15 deprecated wallets at 0 BTC (or minimal dust) +- All 15 deprecated operators removed from allowlist +- 83% cost reduction achieved +- Zero tBTC service interruptions throughout process + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-10-10 +**Next Review**: After first manual sweep execution (TBD) + +**Questions?** Contact Threshold engineering team via #operator-consolidation channel. diff --git a/scripts/wallet-operator-mapper/docs/2025-10-10-operator-consolidation-communication.md b/scripts/wallet-operator-mapper/docs/2025-10-10-operator-consolidation-communication.md new file mode 100644 index 0000000000..75c0099a2f --- /dev/null +++ b/scripts/wallet-operator-mapper/docs/2025-10-10-operator-consolidation-communication.md @@ -0,0 +1,496 @@ +# tBTC Beta Staker Operator Consolidation +## Communication to Node Operators + +**Date**: 2025-10-10 +**Status**: WalletRegistry Audit Submitted +**Project**: Consolidation from 18 to 3 Operators + +--- + +## Executive Summary + +The Threshold DAO is consolidating beta staker operators from **18 nodes to 3 nodes** (1 per provider) to achieve 83% operational cost reduction while maintaining continuous tBTC protocol operation. This communication outlines what you need to know and do during this transition. + +**Key Point**: Your cooperation during this 6-8 week process is essential for a smooth, zero-downtime transition. + +--- + +## What's Changing + +### Before Consolidation +- **18 active operators** across 3 providers (BOAR, STAKED, P2P) +- Each provider runs 6 redundant nodes +- All operators participate in DKG (Distributed Key Generation) +- All operators can coordinate wallet actions + +### After Consolidation +- **3 active operators** (1 per provider) +- 15 operators decommissioned +- Only active operators participate in DKG +- Cost reduction: 83% (~$X annual savings for DAO) + +--- + +## Your Operator Status + +### βœ… ACTIVE OPERATORS (Keep Running) + +If your operator address is listed below, **your node will remain active**: + +| Provider | Operator Address (Keep Active) | +|----------|--------------------------------| +| **STAKED** | `0xf401aae8c639eb1638fd99b90eae8a4c54f9894d` | +| **P2P** | `0xb074a3b960f29a1448a2dd4de95210ca492c18d4` | +| **BOAR** | `0xffb804c2de78576ad011f68a7df63d739b8c8155` | + +**Action Required**: Continue running your node throughout the entire consolidation process. + +--- + +### ❌ DEPRECATED OPERATORS (To Be Decommissioned) + +If your operator is **NOT** in the active list above, it will be decommissioned during this consolidation. + +**15 total operators will be deprecated** (5 from BOAR, 5 from STAKED, 5 from P2P). + +**Your Actions**: +1. Keep your node running until your wallet(s) are fully drained (0 BTC) +2. Monitor your wallet BTC balance (dashboard will be provided) +3. Be available for potential manual sweep coordination (see below) +4. Wait for confirmation from Threshold team before decommissioning +5. Decommission node only after explicit approval + +--- + +## Timeline & Key Dates + +### Phase 0: Smart Contract Audit & Testing (Current Phase) +**Dates**: 2025-10-10 β†’ ~2025-10-27 (~2 weeks) + +- βœ… **Today (2025-10-10)**: WalletRegistry bytecode optimization sent for audit +- βœ… **Already Complete**: Allowlist contract audited +- **Next**: Sepolia testnet deployment for extensive testing +- **Then**: Mainnet deployment preparation + +**Operator Action**: None - continue normal operations + +--- + +### Phase 1: Mainnet Deployment & Weight Updates +**Dates**: ~2025-10-27 β†’ ~2025-10-31 (5 days) + +**What Happens**: +1. Allowlist contract deployed to Ethereum mainnet +2. Optimized WalletRegistry deployed via upgradeable proxy +3. **Weight updates executed**: + - 3 active operators: weight = 100% (full participation) + - 15 deprecated operators: weight = 0% (excluded from new wallets) +4. DKG reconfiguration triggered (only 3 operators in new groups) + +**Operator Action**: +- **Active Operators**: Monitor DKG participation +- **Deprecated Operators**: Confirm you are excluded from new DKG groups (expected behavior) + +--- + +### Phase 2: Natural Wallet Draining +**Dates**: ~2025-10-31 β†’ ~2025-12-01 (4 weeks) + +**What Happens**: +- BTC automatically leaves deprecated wallets through **natural redemptions** +- Bridge prioritizes oldest wallets first (beta staker wallets) +- Monitoring dashboard tracks draining progress in real-time + +**Operator Action**: +- **All Operators**: Monitor your wallet BTC balances via provided dashboard +- **Deprecated Operators**: Watch for your wallets approaching 0 BTC +- **Active Operators**: Continue normal operations + +**No manual intervention required in this phase** (unless volumes are low) + +--- + +### Phase 3: Assessment & Potential Manual Sweeps +**Dates**: ~2025-12-01 β†’ ~2025-12-12 (~2 weeks) + +**Week 4 Assessment**: +- Evaluate draining progress (target: >50% BTC drained) +- Analyze redemption volume trends +- **Decision**: Continue natural draining OR trigger manual sweeps + +**If Manual Sweeps Are Triggered**: + +You may be asked to participate in **manual BTC wallet sweeps** if: +- Your wallet retains >20% balance after 4 weeks, OR +- October deadline is <3 weeks away + +**Manual Sweep Process** (if needed): + +1. **Preparation**: + - Threshold team identifies stragglers (wallets still holding significant BTC) + - Calculate Bitcoin transaction fees (~$50-200 per sweep) + - Construct MovingFunds proposal parameters + +2. **Coordination Call**: + - Scheduled coordination window (every ~3 hours / 900 blocks) + - **51 out of 100 operators must be online** for threshold signing + - Confirm your availability during coordination window + +3. **Execution** (WHERE BTC MOVES): + ``` + Leader operator proposes MovingFunds action + ↓ + 51+ operators participate in threshold signing (RFC-12 coordination) + ↓ + Bitcoin transaction constructed: + - Input: Deprecated wallet's BTC + - Output: Active wallet's BTC address (same provider) + - Fee: Current mempool rate (e.g., 10 sat/vB) + ↓ + Transaction broadcast to Bitcoin network + ↓ + Wait 6 confirmations (~1 hour) + ↓ + BTC now in active wallet + ``` + +4. **Verification**: + - SPV proof submitted to Ethereum Bridge + - Deprecated wallet marked as drained + - Operator becomes eligible for removal + +**Operator Action (if triggered)**: +- **Deprecated Operators**: Be available for threshold signing coordination +- **Active Operators**: May need to participate in threshold signing as part of the 51/100 requirement + +**Cost**: Bitcoin fees (~$50-200) + Ethereum gas (~$100-300) per sweep, paid by DAO treasury + +--- + +### Phase 4: Progressive Operator Removal +**Dates**: ~2025-12-12 β†’ ~2025-12-31 (3 weeks) + +**What Happens**: +- Operators removed **progressively** as their wallets reach 0 BTC (not all at once) +- Each operator must have 0 BTC for **1+ week** before removal (safety buffer) +- Three removal batches: Week 6, Week 7, Week 8 + +**Removal Protocol**: +1. Verify wallet at 0 BTC for 1+ week +2. Confirm no pending coordination actions +3. Threshold team notifies provider +4. Provider confirms readiness +5. Governance removes operator from allowlist +6. Provider decommissions node infrastructure + +**Operator Action**: +- **Deprecated Operators**: + - Monitor your wallet balance (provided dashboard) + - Wait for explicit decommissioning approval from Threshold team + - **DO NOT** decommission your node until receiving confirmation + - Once approved, shut down node and confirm to Threshold team +- **Active Operators**: Continue normal operations, monitor stability + +**Batched Removal Schedule**: +- **Week 6** (~2025-12-12): First batch (operators with 0 BTC confirmed 1+ week) +- **Week 7** (~2025-12-19): Second batch (additional drained operators) +- **Week 8** (~2025-12-26): Final cleanup (remaining operators, edge cases) + +--- + +## What You Need to Do + +### Immediate Actions (This Week) + +1. **Verify Your Operator Status**: + - Check if your operator address is in the "Active" or "Deprecated" list above + - Contact Threshold team if unclear + +2. **Ensure Contact Information is Current**: + - Verify you're in the operator coordination channels (Slack/Discord) + - Update email and emergency contact info + +3. **Review Node Infrastructure**: + - Ensure nodes are healthy and monitoring is operational + - Verify you can access node logs and status + +--- + +### Ongoing Actions (Throughout Consolidation) + +#### For ACTIVE Operators: + +1. **Keep Your Node Running**: + - Maintain 99.5%+ uptime throughout consolidation + - Monitor DKG participation + +2. **Monitor Coordination Windows**: + - RFC-12 coordination with 3 operators (down from 18) + - Report any coordination issues immediately + +3. **Be Available for Manual Sweeps** (if triggered): + - Respond to coordination requests + - Participate in threshold signing (51/100 requirement) + +--- + +#### For DEPRECATED Operators: + +1. **Keep Your Node Running** (until approved for decommissioning): + - **Critical**: Do NOT shut down prematurely + - Your node may be needed for manual sweeps + - Your wallet holds valuable BTC that must be drained first + +2. **Monitor Your Wallet Balance**: + - Watch BTC balance decrease over time + - Alert Threshold team if balance stagnates for >2 weeks + +3. **Be Available for Manual Sweep Coordination** (if needed): + - Check coordination channel daily during Weeks 4-5 + - Respond within 4 hours to coordination requests + - Ensure your node can participate in threshold signing + +4. **Wait for Decommissioning Approval**: + - You will receive explicit confirmation when: + - Your wallet is at 0 BTC for 1+ week + - Operator removal transaction is executed + - Safe to shut down node + - **DO NOT** decommission based on 0 BTC alone - wait for team confirmation + +5. **Decommission When Approved**: + - Shut down node infrastructure + - Confirm decommissioning to Threshold team + - Provide final node status report + +--- + +## Critical Information + +### No T Token Movement Required + +**Important**: This consolidation does NOT involve T token staking or movement. + +- Original system required T tokens staked via TokenStaking contract +- Post-TIP-92/TIP-100: Allowlist uses **weight-based authorization** (0-100 weights) +- Setting `weight = 0` is just a state change, NOT a fund transfer +- **You do NOT need to move, unstake, or manage any T tokens** + +Only **Bitcoin (BTC)** moves during this process (from deprecated wallets to active wallets). + +--- + +### Bitcoin Movement - What Actually Happens + +#### Scenario 1: Natural Draining (Preferred) + +**No operator coordination needed** - happens automatically: + +``` +User redeems tBTC + ↓ +Bridge selects oldest wallet (deprecated beta staker wallet) + ↓ +BTC leaves deprecated wallet β†’ goes to user's Bitcoin address + ↓ +Wallet drains naturally over 4-7 weeks +``` + +**Operators do nothing** - just monitor BTC balance decreasing via dashboard. + +--- + +#### Scenario 2: Manual Sweeps (Fallback) + +**Operator coordination IS required** - BTC moved between wallets. + +**Per-Provider Bitcoin Movements**: + +| Provider | From (Deprecated Wallets) | To (Active Wallet) | BTC Amount | +|----------|---------------------------|-------------------|------------| +| BOAR | 5 old wallet addresses | 0xffb8...8155's BTC wallet | Sum of 5 wallets' BTC | +| STAKED | 5 old wallet addresses | 0xf401...9894d's BTC wallet | Sum of 5 wallets' BTC | +| P2P | 5 old wallet addresses | 0xb074...18d4's BTC wallet | Sum of 5 wallets' BTC | + +**Important**: Each provider sweeps their own deprecated wallets to their own active wallet. Your BTC stays within your provider organization. + +--- + +### Threshold Signing Requirements + +For manual sweeps to succeed, **51 out of 100 operators must be online simultaneously**. + +**With 3 providers Γ— 6 operators each = 18 operators total**: +- Need **~10 operators online** at the same time (51% of 18) +- Each provider should have **3-4 operators available minimum** + +**Coordination Windows**: +- Every **900 blocks** (~3 hours on Ethereum) +- If operators miss window β†’ retry next window (3 hours later) + +**Your Commitment**: When manual sweep coordination is scheduled, please ensure your node is online and responsive. + +--- + +### Weekly Status Reports + +Starting Week 3, you'll receive **weekly updates** with: +- Overall consolidation progress (% BTC drained) +- Your specific wallet status +- Upcoming milestones and actions +- Any coordination requests + +--- + +## Communication Channels + +### Primary Channels + +1. **Operator Coordination Channel** (Slack/Discord): + - Daily updates during critical phases + - Manual sweep coordination requests + - Emergency communications + +2. **Updates**: + - Weekly status reports + - Important milestone announcements + - Decommissioning approvals + +3. **Emergency Contact**: + - Critical issues requiring immediate response + - Node coordination failures + - Unexpected BTC balance changes + +**Please ensure you're subscribed to all channels and check them daily during Weeks 4-8.** + +--- + +## Frequently Asked Questions (FAQ) + +### Q: Do I need to move or unstake T tokens? +**A**: No. This consolidation uses weight-based authorization. No T token movement is required. + +--- + +### Q: When exactly should I shut down my deprecated node? +**A**: Only after you receive **explicit written confirmation** from the Threshold team that: +1. Your wallet has been at 0 BTC for 1+ week +2. Operator removal transaction has been executed on-chain +3. You are approved to decommission + +**Do not shut down based on 0 BTC balance alone.** + +--- + +### Q: What happens if I'm unavailable during a manual sweep window? +**A**: The sweep will retry in the next coordination window (~3 hours later). However, repeated unavailability may delay the consolidation timeline. Please make best efforts to be available during Weeks 4-5 if manual sweeps are triggered. + +--- + +### Q: Will I receive compensation for participating in manual sweeps? +**A**: Manual sweep costs (Bitcoin fees + Ethereum gas) are paid by the DAO treasury, not individual operators. No additional compensation is provided beyond your standard node operation agreement. + +--- + +### Q: What if my wallet balance doesn't drain naturally? +**A**: The monitoring dashboard will alert the Threshold team if your wallet isn't draining after 2 weeks. We'll work with you to coordinate manual sweeps during Week 4-5 assessment. + +--- + +### Q: Can I decommission my node early if I see 0 BTC balance? +**A**: **No.** You must wait for explicit approval from Threshold team. The 1-week safety buffer at 0 BTC ensures no pending transactions. Early decommissioning could cause coordination issues. + +--- + +### Q: Will there be any downtime during the consolidation? +**A**: **No.** The entire process is designed for **zero-downtime** tBTC service. Users will not experience any interruption in minting or redemptions. + +--- + +### Q: How will I know if manual sweeps are triggered? +**A**: You'll receive: +1. Notification from Threshold team +2. Message in operator coordination channel +3. Calendar invitation for coordination window +4. Dashboard alert + +You'll have at least **48 hours notice** before manual sweep coordination. + +--- + +### Q: What happens if DKG fails with only 3 operators? +**A**: RFC-12 coordination has been battle-tested for 21+ months and supports 3-operator configurations. If any issues arise, we can temporarily rollback weight updates to include more operators. This is a low-probability risk with a proven mitigation plan. + +--- + +## Support & Contact + +### Technical Support +- **Email**: [support email - TBD] +- **Slack/Discord**: TBD +- **Emergency**: [emergency contact - TBD] + +### Project Manager +- **Name**: [PM name - TBD] +- **Email**: [PM email - TBD] +- **Availability**: Monday-Friday, 9am-5pm UTC + +--- + +## Next Steps for You + +### This Week (2025-10-10 β†’ 2025-10-17) + +- [ ] Verify your operator status (active or deprecated) +- [ ] Confirm you're in all communication channels +- [ ] Review node health and monitoring +- [ ] Update contact information if needed +- [ ] Acknowledge receipt of this communication (reply to coordinator) + +### Week of 2025-10-17 + +- [ ] Wait for audit completion announcement +- [ ] Monitor for Sepolia testnet deployment +- [ ] Review testnet results (if shared) + +### Week of 2025-10-27 (Mainnet Deployment) + +- [ ] Monitor for mainnet deployment announcement +- [ ] Verify weight updates on-chain +- [ ] **Active Operators**: Confirm DKG participation increases +- [ ] **Deprecated Operators**: Confirm exclusion from new DKG groups + +### Weeks of 2025-10-31 β†’ 2025-12-01 (Natural Draining) + +- [ ] Access monitoring dashboard (link will be provided) +- [ ] Monitor your wallet BTC balance weekly +- [ ] Report any anomalies (balance increase, stagnation >2 weeks) + +### Week of 2025-12-01 (Assessment) + +- [ ] Review Week 4 assessment results +- [ ] **If manual sweeps triggered**: Respond to coordination requests within 4 hours +- [ ] **If natural draining continues**: Continue monitoring + +### Weeks of 2025-12-12 β†’ 2025-12-31 (Operator Removal) + +- [ ] **Deprecated Operators**: Monitor for decommissioning approval +- [ ] **Deprecated Operators**: Decommission only after explicit confirmation +- [ ] **Active Operators**: Monitor coordination stability with reduced operator set + +--- + +## Summary: Critical Dates & Actions + +| Date Range | Phase | Your Action | +|------------|-------|-------------| +| **2025-10-10 β†’ 2025-10-27** | Audit & Testing | None - continue normal operations | +| **2025-10-27 β†’ 2025-10-31** | Mainnet Deployment | Monitor DKG participation changes | +| **2025-10-31 β†’ 2025-12-01** | Natural Draining | Monitor wallet balance via dashboard | +| **2025-12-01 β†’ 2025-12-12** | Assessment & Sweeps | Be available for manual sweep coordination (if triggered) | +| **2025-12-12 β†’ 2025-12-31** | Operator Removal | **Deprecated**: Wait for decommissioning approval; **Active**: Continue operations | + +--- + +**Document Version**: 1.0 +**Last Updated**: 2025-10-10 +**Next Update**: After audit/testing completion (~2025-10-27) diff --git a/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-README.md b/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-README.md new file mode 100644 index 0000000000..52d3f27d76 --- /dev/null +++ b/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-README.md @@ -0,0 +1,358 @@ +# Manual Sweep Execution Script + +**Created**: 2025-10-20 +**Source**: 2025-10-10-manual-sweep-technical-process.md +**Script**: 2025-10-20-manual-sweep-execution-script.sh + +## Overview + +This shell script template extracts and organizes all executable commands from the Manual Sweep Technical Specification into a runnable format. It guides operators and the Threshold team through the 8-phase manual sweep process. + +## What This Script Does + +### βœ… Automated Phases +- **Phase 1**: Query wallet balances, UTXOs, and mempool fees +- **Phase 2**: Calculate next coordination window +- **Phase 6**: Monitor Bitcoin confirmations +- **Phase 8**: Verify final state and generate notifications + +### ⚠️ Manual Intervention Required +- **Phases 3-5**: Operator node coordination (automatic via node software) +- **Phase 7**: SPV proof construction (requires specialized tools) +- **Notifications**: Sending alerts to operators + +## Prerequisites + +### Required Commands +- `cast` (Foundry) - Ethereum contract interactions +- `bitcoin-cli` - Bitcoin node RPC +- `curl` - HTTP requests +- `jq` - JSON parsing +- `bc` - Arithmetic calculations + +### Configuration Required + +Before running, you MUST customize these variables in the script: + +```bash +# Ethereum Contracts +ALLOWLIST_ADDRESS="0x..." +WALLET_REGISTRY_ADDRESS="0x..." +BRIDGE_ADDRESS="0x..." +VALIDATOR_ADDRESS="0x..." + +# Wallets +DEPRECATED_WALLET_PKH="0x..." +DEPRECATED_WALLET_ADDRESS="bc1q..." +ACTIVE_WALLET_PKH="0x..." +ACTIVE_WALLET_ADDRESS="bc1q..." + +# Provider +PROVIDER="BOAR" # or "STAKED" or "P2P" +``` + +## Usage + +### Interactive Menu Mode + +```bash +./2025-10-20-manual-sweep-execution-script.sh +``` + +This presents a menu to run individual phases: + +``` +1. Phase 1: Preparation +2. Phase 2: Coordination Window +3. Phases 3-5: Operator Coordination (monitoring) +4. Phase 6: Bitcoin Confirmations +5. Phase 7: SPV Proof Submission +6. Phase 8: Verification and Cleanup +7. Check Operator Node Health +8. Run Complete Process +``` + +### Running Specific Phases + +You can modify the script to run specific functions directly: + +```bash +# Run only preparation +source ./2025-10-20-manual-sweep-execution-script.sh +phase1_preparation + +# Check operator health +check_operator_node_health + +# Monitor Bitcoin confirmations +phase6_bitcoin_confirmations +``` + +## Phase-by-Phase Guide + +### Phase 1: Preparation + +**Purpose**: Identify straggler wallets and construct proposal parameters + +**Commands executed**: +```bash +# Query Bitcoin balance +bitcoin-cli getreceivedbyaddress
0 + +# Query Ethereum wallet state +cast call "getWalletMainUtxo(bytes20)" + +# Get mempool fees +curl https://mempool.space/api/v1/fees/recommended + +# List UTXOs +bitcoin-cli listunspent 0 9999999 '["
"]' +``` + +**Output**: `proposal_params.json` with all MovingFunds parameters + +### Phase 2: Coordination Window + +**Purpose**: Calculate when the next RFC-12 coordination window opens + +**Commands executed**: +```bash +# Get current block +cast block-number + +# Calculate next coordination block (every 900 blocks) +``` + +**Output**: Coordination block number and timing + +### Phases 3-5: Operator Coordination + +**Purpose**: Monitor automatic coordination process + +**Note**: These phases are handled automatically by operator node software. This script provides monitoring guidance only. + +**Expected flow**: +1. Leader is deterministically selected +2. Leader broadcasts CoordinationMessage +3. Followers validate and sign +4. Leader aggregates signatures +5. Bitcoin transaction is broadcast + +**Manual action**: Monitor coordination channel for Bitcoin transaction ID + +### Phase 6: Bitcoin Confirmations + +**Purpose**: Wait for 6 Bitcoin confirmations and verify transfer + +**Commands executed**: +```bash +# Monitor confirmations (loops) +bitcoin-cli gettransaction + +# Verify UTXO arrived +bitcoin-cli gettxout +bitcoin-cli getreceivedbyaddress 0 +``` + +**Output**: Confirmation when 6+ blocks reached + +### Phase 7: SPV Proof Submission + +**Purpose**: Construct and submit SPV proof to Ethereum Bridge + +**Commands executed**: +```bash +# Get raw transaction +bitcoin-cli getrawtransaction + +# Get block data with merkle tree +bitcoin-cli getblock 2 + +# Get block header +bitcoin-cli getblockheader false +``` + +**Manual action required**: +- Build Merkle proof using specialized tools +- Submit to Bridge contract with `cast send` + +**Note**: SPV proof construction requires tBTC-specific tooling not included in this script. + +### Phase 8: Verification + +**Purpose**: Verify final state and generate notifications + +**Commands executed**: +```bash +# Check Ethereum state +cast call "getWalletMainUtxo(bytes20)" + +# Check Bitcoin balance +bitcoin-cli getreceivedbyaddress 0 +``` + +**Output**: Success notification text file + +## Output Files + +All intermediate results are saved to a timestamped directory: +``` +/tmp/manual-sweep-YYYYMMDD-HHMMSS/ +β”œβ”€β”€ sweep.log # Complete execution log +β”œβ”€β”€ wallet_utxo.txt # Ethereum wallet state +β”œβ”€β”€ mempool_fees.json # Current Bitcoin fees +β”œβ”€β”€ utxos.json # All wallet UTXOs +β”œβ”€β”€ proposal_params.json # MovingFunds proposal +β”œβ”€β”€ coordination_block.txt # Coordination window +β”œβ”€β”€ bitcoin_txid.txt # Bitcoin transaction ID +β”œβ”€β”€ block_hash.txt # Bitcoin block hash +β”œβ”€β”€ raw_tx.hex # Raw transaction +β”œβ”€β”€ block_data.json # Block with merkle tree +β”œβ”€β”€ tx_index.txt # Transaction index +β”œβ”€β”€ block_header.hex # Block header +β”œβ”€β”€ deprecated_wallet_final_state.txt # Final Ethereum state +└── success_notification.txt # Operator notification +``` + +## Example Workflow + +### Complete Manual Sweep Process + +```bash +# 1. Configure script variables (edit file) +vim 2025-10-20-manual-sweep-execution-script.sh + +# 2. Run preparation phase +./2025-10-20-manual-sweep-execution-script.sh +# Select option 1: Phase 1 Preparation + +# 3. Send notifications to operators (manual) +# Email/Slack the proposal_params.json + +# 4. Calculate coordination window +# Select option 2: Phase 2 Coordination Window + +# 5. Wait for coordination window +# Monitor operator nodes during window + +# 6. After Bitcoin TX is broadcast, monitor confirmations +# Select option 4: Phase 6 Bitcoin Confirmations +# Enter Bitcoin transaction ID when prompted + +# 7. Construct and submit SPV proof (manual + script) +# Select option 5: Phase 7 SPV Proof Submission +# Follow instructions to complete SPV proof + +# 8. Verify and notify +# Select option 6: Phase 8 Verification +# Send success_notification.txt to operators +``` + +## Safety Features + +- **Set -e**: Exits on any command error +- **Set -u**: Exits on undefined variables +- **Logging**: All actions logged with timestamps +- **Output preservation**: All intermediate data saved +- **Prerequisite checks**: Validates required commands exist + +## Limitations + +### What This Script CANNOT Do + +1. **Operator node coordination**: Phases 3-5 require running operator nodes with tBTC software +2. **SPV proof construction**: Requires specialized tBTC tools (not just Bitcoin RPC) +3. **Automatic notifications**: Sending emails/Slack messages to operators +4. **Signature aggregation**: This is done by operator node software +5. **Leader selection**: Deterministic but requires node coordination protocol + +### What This Script CAN Do + +1. βœ… Query all necessary data (wallets, UTXOs, fees) +2. βœ… Calculate proposal parameters +3. βœ… Monitor Bitcoin confirmations +4. βœ… Verify final state +5. βœ… Generate notification templates + +## Security Considerations + +### ⚠️ WARNING: Private Keys + +The script requires `SUBMITTER_PRIVATE_KEY` for SPV proof submission. + +**Best practices**: +- Use environment variable instead of hardcoding +- Use hardware wallet or secure key management +- Only grant this key permission to submit proofs (no other powers) + +### Recommended Approach + +```bash +# Don't hardcode in script +export SUBMITTER_PRIVATE_KEY="0x..." + +# Reference in script +SUBMITTER_PRIVATE_KEY="${SUBMITTER_PRIVATE_KEY:-}" +``` + +## Troubleshooting + +### Bitcoin CLI Connection Issues +```bash +# Check Bitcoin node is running +bitcoin-cli getblockchaininfo + +# If failing, check bitcoin.conf settings +``` + +### Ethereum RPC Issues +```bash +# Test connection +cast block-number + +# Check RPC URL in environment +echo $ETH_RPC_URL +``` + +### Missing Commands +```bash +# Install Foundry (for cast) +curl -L https://foundry.paradigm.xyz | bash +foundryup + +# Bitcoin Core required for bitcoin-cli +``` + +## Cost Estimation + +When running Phase 1, the script calculates expected costs: + +- **Bitcoin fee**: `TX_SIZE Γ— FEE_RATE` satoshis +- **Ethereum gas**: Manual SPV proof submission (~400K gas) + +Check current prices before execution: +```bash +# Bitcoin mempool +curl https://mempool.space/api/v1/fees/recommended + +# Ethereum gas +cast gas-price +``` + +## Next Steps After Running + +1. **Week 4 Assessment**: Use Phase 1 to identify which wallets need sweeps +2. **Schedule Coordination**: Based on Phase 2 calculation, schedule operator availability +3. **Execute Sweep**: Run through all phases with operator participation +4. **Safety Buffer**: Wait 1 week after wallet reaches 0 BTC +5. **Operator Removal**: Proceed with allowlist updates and node decommissioning + +## Support + +- Review full technical specification: `2025-10-10-manual-sweep-technical-process.md` +- Check operator node logs during coordination +- Contact Threshold engineering team via `#operator-consolidation` + +## License + +This script is part of the tBTC Beta Staker Consolidation project. diff --git a/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-execution-script.sh b/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-execution-script.sh new file mode 100755 index 0000000000..1cb903fb02 --- /dev/null +++ b/scripts/wallet-operator-mapper/docs/2025-10-20-manual-sweep-execution-script.sh @@ -0,0 +1,537 @@ +#!/bin/bash +# +# Manual Sweep Execution Script +# tBTC Beta Staker Consolidation - Manual MovingFunds Process +# +# Created: 2025-10-20 +# Based on: 2025-10-10-manual-sweep-technical-process.md +# +# WARNING: This is a TEMPLATE script. Review and customize all variables before execution. +# This script requires manual intervention at various steps. +# + +set -e # Exit on error +set -u # Exit on undefined variable + +#============================================================================== +# CONFIGURATION VARIABLES - CUSTOMIZE THESE BEFORE RUNNING +#============================================================================== + +# Ethereum Configuration +ALLOWLIST_ADDRESS="0x_YOUR_ALLOWLIST_CONTRACT_ADDRESS" +WALLET_REGISTRY_ADDRESS="0x_YOUR_WALLET_REGISTRY_ADDRESS" +BRIDGE_ADDRESS="0x_YOUR_BRIDGE_CONTRACT_ADDRESS" +VALIDATOR_ADDRESS="0x_YOUR_WALLET_PROPOSAL_VALIDATOR_ADDRESS" + +# Operator Configuration +OPERATOR_ADDRESS="0x_YOUR_OPERATOR_ADDRESS" +SUBMITTER_PRIVATE_KEY="YOUR_ETHEREUM_PRIVATE_KEY" # For SPV proof submission + +# Wallet Configuration (example - replace with actual values) +DEPRECATED_WALLET_PKH="0x1234567890abcdef1234567890abcdef12345678" +DEPRECATED_WALLET_ADDRESS="bc1q_YOUR_DEPRECATED_WALLET_ADDRESS" +ACTIVE_WALLET_PKH="0xffb804c2de78576ad011f68a7df63d739b8c8155" +ACTIVE_WALLET_ADDRESS="bc1q_YOUR_ACTIVE_WALLET_ADDRESS" + +# Provider (BOAR, STAKED, or P2P) +PROVIDER="BOAR" + +# Coordination Configuration +COORDINATION_FREQUENCY=900 # blocks +COORDINATION_WINDOW_SIZE=100 # blocks + +# Bitcoin Configuration +REQUIRED_CONFIRMATIONS=6 + +# Output file for storing intermediate results +OUTPUT_DIR="/tmp/manual-sweep-$(date -u +%Y%m%d-%H%M%S)" +mkdir -p "$OUTPUT_DIR" + +#============================================================================== +# UTILITY FUNCTIONS +#============================================================================== + +log() { + echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$OUTPUT_DIR/sweep.log" +} + +error() { + echo "[ERROR] $(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" | tee -a "$OUTPUT_DIR/sweep.log" >&2 + exit 1 +} + +check_command() { + if ! command -v "$1" &> /dev/null; then + error "Required command '$1' not found. Please install it." + fi +} + +#============================================================================== +# PREREQUISITE CHECKS +#============================================================================== + +log "Starting prerequisite checks..." + +# Check required commands +check_command "cast" +check_command "bitcoin-cli" +check_command "curl" +check_command "jq" + +log "All prerequisites satisfied." + +#============================================================================== +# PHASE 1: PREPARATION (Threshold Team) +#============================================================================== + +phase1_preparation() { + log "=== PHASE 1: PREPARATION ===" + + # Step 1.1: Identify Straggler Wallets + log "Step 1.1: Querying wallet balance..." + + # Query Bitcoin wallet balance + BTC_BALANCE=$(bitcoin-cli getreceivedbyaddress "$DEPRECATED_WALLET_ADDRESS" 0 || echo "0") + log "Bitcoin balance: $BTC_BALANCE BTC" + + # Query Ethereum wallet state + log "Querying Ethereum wallet state..." + cast call "$WALLET_REGISTRY_ADDRESS" \ + "getWalletMainUtxo(bytes20)" \ + "$DEPRECATED_WALLET_PKH" > "$OUTPUT_DIR/wallet_utxo.txt" + + log "Wallet UTXO saved to $OUTPUT_DIR/wallet_utxo.txt" + + # Step 1.2: Calculate Bitcoin Transaction Fees + log "Step 1.2: Checking current mempool fee recommendations..." + + curl -s https://mempool.space/api/v1/fees/recommended > "$OUTPUT_DIR/mempool_fees.json" + + FASTEST_FEE=$(jq -r '.fastestFee' "$OUTPUT_DIR/mempool_fees.json") + HALF_HOUR_FEE=$(jq -r '.halfHourFee' "$OUTPUT_DIR/mempool_fees.json") + HOUR_FEE=$(jq -r '.hourFee' "$OUTPUT_DIR/mempool_fees.json") + + log "Fastest fee: $FASTEST_FEE sat/vB" + log "Half hour fee: $HALF_HOUR_FEE sat/vB" + log "Hour fee: $HOUR_FEE sat/vB" + + # Use half hour fee as default (balanced approach) + FEE_RATE="$HALF_HOUR_FEE" + log "Selected fee rate: $FEE_RATE sat/vB" + + # Step 1.3: Query Wallet UTXOs + log "Step 1.3: Querying wallet UTXOs..." + + # Using Bitcoin Core RPC + bitcoin-cli listunspent 0 9999999 "[\"$DEPRECATED_WALLET_ADDRESS\"]" > "$OUTPUT_DIR/utxos.json" + + UTXO_COUNT=$(jq '. | length' "$OUTPUT_DIR/utxos.json") + log "Found $UTXO_COUNT UTXOs" + + # Display UTXOs + jq -r '.[] | "UTXO: \(.txid):\(.vout) = \(.amount) BTC"' "$OUTPUT_DIR/utxos.json" + + # Step 1.4: Construct MovingFunds Proposal Parameters + log "Step 1.4: Constructing MovingFunds proposal..." + + # Calculate total input value + TOTAL_INPUT_BTC=$(jq '[.[].amount] | add' "$OUTPUT_DIR/utxos.json") + TOTAL_INPUT_SATS=$(echo "$TOTAL_INPUT_BTC * 100000000" | bc | cut -d. -f1) + + # Estimate transaction size (10 base + 148*inputs + 34*outputs) + TX_SIZE=$((10 + 148 * UTXO_COUNT + 34)) + TOTAL_FEE_SATS=$((TX_SIZE * FEE_RATE)) + OUTPUT_VALUE_SATS=$((TOTAL_INPUT_SATS - TOTAL_FEE_SATS)) + + log "Total input: $TOTAL_INPUT_SATS sats" + log "Estimated tx size: $TX_SIZE bytes" + log "Total fee: $TOTAL_FEE_SATS sats" + log "Output value: $OUTPUT_VALUE_SATS sats" + + # Save proposal parameters + cat > "$OUTPUT_DIR/proposal_params.json" < "$OUTPUT_DIR/coordination_block.txt" +} + +#============================================================================== +# PHASE 3-5: OPERATOR NODE COORDINATION (AUTOMATIC) +#============================================================================== + +phase3_5_operator_coordination() { + log "=== PHASES 3-5: OPERATOR NODE COORDINATION ===" + log "" + log "AUTOMATIC PROCESS - Handled by operator nodes" + log "" + log "Operator nodes will automatically:" + log " - Phase 3: Leader proposes MovingFunds" + log " - Phase 4: Followers validate and sign" + log " - Phase 5: Leader aggregates signatures and broadcasts Bitcoin transaction" + log "" + log "MANUAL ACTION: Monitor coordination channel and operator node logs" + log "" + log "Expected outputs:" + log " - CoordinationMessage broadcast" + log " - Threshold signing (51+ signatures)" + log " - Bitcoin transaction broadcast" + log "" + log "Wait for Bitcoin transaction ID announcement..." +} + +#============================================================================== +# PHASE 6: BITCOIN CONFIRMATIONS +#============================================================================== + +phase6_bitcoin_confirmations() { + log "=== PHASE 6: BITCOIN CONFIRMATIONS ===" + + # Get transaction ID (manual input or from file) + if [ -f "$OUTPUT_DIR/bitcoin_txid.txt" ]; then + BITCOIN_TXID=$(cat "$OUTPUT_DIR/bitcoin_txid.txt") + else + read -p "Enter Bitcoin transaction ID: " BITCOIN_TXID + echo "$BITCOIN_TXID" > "$OUTPUT_DIR/bitcoin_txid.txt" + fi + + log "Monitoring transaction: $BITCOIN_TXID" + + # Step 6.1: Wait for confirmations + log "Step 6.1: Waiting for $REQUIRED_CONFIRMATIONS confirmations..." + + while true; do + TX_INFO=$(bitcoin-cli gettransaction "$BITCOIN_TXID" 2>/dev/null || echo "{}") + CONFIRMATIONS=$(echo "$TX_INFO" | jq -r '.confirmations // 0') + + log "Current confirmations: $CONFIRMATIONS / $REQUIRED_CONFIRMATIONS" + + if [ "$CONFIRMATIONS" -ge "$REQUIRED_CONFIRMATIONS" ]; then + log "βœ… Transaction confirmed with $CONFIRMATIONS confirmations" + BLOCK_HASH=$(echo "$TX_INFO" | jq -r '.blockhash') + log "Block hash: $BLOCK_HASH" + echo "$BLOCK_HASH" > "$OUTPUT_DIR/block_hash.txt" + break + fi + + log "Waiting 60 seconds before next check..." + sleep 60 + done + + # Step 6.2: Verify BTC arrived at target wallet + log "Step 6.2: Verifying BTC arrived at target wallet..." + + TARGET_BALANCE=$(bitcoin-cli getreceivedbyaddress "$ACTIVE_WALLET_ADDRESS" 0) + log "Active wallet balance: $TARGET_BALANCE BTC" + + # Check specific UTXO + VOUT=0 # Typically output 0 + UTXO_INFO=$(bitcoin-cli gettxout "$BITCOIN_TXID" $VOUT 2>/dev/null || echo "null") + + if [ "$UTXO_INFO" != "null" ]; then + VALUE=$(echo "$UTXO_INFO" | jq -r '.value') + ADDRESS=$(echo "$UTXO_INFO" | jq -r '.scriptPubKey.address') + log "βœ… UTXO verified: $VALUE BTC to $ADDRESS" + else + log "⚠️ Warning: Could not verify UTXO (may be already spent)" + fi +} + +#============================================================================== +# PHASE 7: SPV PROOF SUBMISSION +#============================================================================== + +phase7_spv_proof() { + log "=== PHASE 7: SPV PROOF SUBMISSION ===" + + BITCOIN_TXID=$(cat "$OUTPUT_DIR/bitcoin_txid.txt") + BLOCK_HASH=$(cat "$OUTPUT_DIR/block_hash.txt") + + # Step 7.1: Construct SPV Proof + log "Step 7.1: Constructing SPV proof..." + log "⚠️ WARNING: SPV proof construction requires specialized tools" + log "" + log "Manual steps required:" + log " 1. Get raw transaction: bitcoin-cli getrawtransaction $BITCOIN_TXID" + log " 2. Get block data: bitcoin-cli getblock $BLOCK_HASH 2" + log " 3. Build Merkle proof from transaction position in block" + log " 4. Get block headers (current + 5 preceding for 6 confirmations)" + log "" + + # Get raw transaction + log "Fetching raw transaction..." + RAW_TX=$(bitcoin-cli getrawtransaction "$BITCOIN_TXID") + echo "$RAW_TX" > "$OUTPUT_DIR/raw_tx.hex" + log "Raw transaction saved to $OUTPUT_DIR/raw_tx.hex" + + # Get block data + log "Fetching block data..." + bitcoin-cli getblock "$BLOCK_HASH" 2 > "$OUTPUT_DIR/block_data.json" + log "Block data saved to $OUTPUT_DIR/block_data.json" + + # Get transaction index in block + TX_INDEX=$(jq -r ".tx | map(.txid) | index(\"$BITCOIN_TXID\")" "$OUTPUT_DIR/block_data.json") + log "Transaction index in block: $TX_INDEX" + echo "$TX_INDEX" > "$OUTPUT_DIR/tx_index.txt" + + # Get block header + log "Fetching block headers..." + bitcoin-cli getblockheader "$BLOCK_HASH" false > "$OUTPUT_DIR/block_header.hex" + + log "⚠️ MANUAL ACTION: Complete SPV proof construction" + log "Required data collected in: $OUTPUT_DIR/" + log " - raw_tx.hex: Bitcoin transaction" + log " - block_data.json: Block with merkle tree" + log " - tx_index.txt: Transaction position" + log " - block_header.hex: Block header" + log "" + log "Use tBTC SPV proof construction tools to build complete proof" + log "" + + # Step 7.2: Submit SPV Proof (manual - requires constructed proof) + log "Step 7.2: Ready to submit SPV proof to Ethereum Bridge" + log "" + log "When SPV proof is ready, submit with:" + log "" + log "cast send $BRIDGE_ADDRESS \\" + log " \"submitMovingFundsProof(bytes,bytes,bytes,uint256)\" \\" + log " \$BITCOIN_TX_HEX \\" + log " \$MERKLE_PROOF_HEX \\" + log " \$BLOCK_HEADERS_HEX \\" + log " \$TX_INDEX \\" + log " --private-key \$SUBMITTER_PRIVATE_KEY \\" + log " --gas-limit 500000" + log "" + log "⚠️ Ensure gas price is reasonable before submitting" + log "Check current gas: cast gas-price" +} + +#============================================================================== +# PHASE 8: VERIFICATION AND CLEANUP +#============================================================================== + +phase8_verification() { + log "=== PHASE 8: VERIFICATION AND CLEANUP ===" + + # Step 8.1: Verify wallet balance on-chain (Ethereum) + log "Step 8.1: Verifying deprecated wallet balance on-chain (Ethereum)..." + + cast call "$WALLET_REGISTRY_ADDRESS" \ + "getWalletMainUtxo(bytes20)" \ + "$DEPRECATED_WALLET_PKH" > "$OUTPUT_DIR/deprecated_wallet_final_state.txt" + + log "Deprecated wallet final state saved to $OUTPUT_DIR/deprecated_wallet_final_state.txt" + log "Expected: Empty UTXO (all zeros)" + cat "$OUTPUT_DIR/deprecated_wallet_final_state.txt" + + # Step 8.2: Verify wallet balance off-chain (Bitcoin) + log "Step 8.2: Verifying deprecated wallet balance off-chain (Bitcoin)..." + + FINAL_BALANCE=$(bitcoin-cli getreceivedbyaddress "$DEPRECATED_WALLET_ADDRESS" 0) + log "Deprecated wallet final balance: $FINAL_BALANCE BTC" + + if [ "$(echo "$FINAL_BALANCE < 0.00001" | bc)" -eq 1 ]; then + log "βœ… Wallet effectively empty (dust acceptable)" + else + log "⚠️ Warning: Wallet still has significant balance: $FINAL_BALANCE BTC" + fi + + # Step 8.3: Mark operator eligible for removal + log "Step 8.3: Marking operator eligible for removal..." + log "Operator: $DEPRECATED_WALLET_PKH" + log "Status: AWAITING_REMOVAL" + log "Eligible for removal after: $(date -u -d '+7 days' +%Y-%m-%d) (1 week safety buffer)" + + # Step 8.4: Generate success notification + log "Step 8.4: Generating success notification..." + + BITCOIN_TXID=$(cat "$OUTPUT_DIR/bitcoin_txid.txt") + + cat > "$OUTPUT_DIR/success_notification.txt" </dev/null; then + log "βœ… tBTC node service is running" + else + log "❌ tBTC node service is NOT running" + fi + + # Check node health endpoint + if curl -s http://localhost:8080/health > /dev/null 2>&1; then + HEALTH=$(curl -s http://localhost:8080/health) + log "Node health: $HEALTH" + else + log "⚠️ Cannot reach node health endpoint" + fi + + # Check Ethereum sync + ETH_BLOCK=$(cast block-number 2>/dev/null || echo "ERROR") + log "Ethereum block height: $ETH_BLOCK" + + # Check Bitcoin connection + if bitcoin-cli getblockchaininfo > /dev/null 2>&1; then + BTC_BLOCKS=$(bitcoin-cli getblockchaininfo | jq -r '.blocks') + log "βœ… Bitcoin node connected, blocks: $BTC_BLOCKS" + else + log "❌ Cannot connect to Bitcoin node" + fi +} + +#============================================================================== +# MAIN EXECUTION +#============================================================================== + +main() { + log "Manual Sweep Execution Script Started" + log "Output directory: $OUTPUT_DIR" + log "" + + # Show menu + echo "==========================================" + echo "Manual Sweep Execution - Main Menu" + echo "==========================================" + echo "1. Phase 1: Preparation (identify wallets, calculate fees)" + echo "2. Phase 2: Coordination Window (calculate next window)" + echo "3. Phases 3-5: Operator Coordination (automatic - monitoring only)" + echo "4. Phase 6: Bitcoin Confirmations (monitor confirmations)" + echo "5. Phase 7: SPV Proof Submission (construct and submit)" + echo "6. Phase 8: Verification and Cleanup" + echo "7. Check Operator Node Health" + echo "8. Run Complete Process (all phases)" + echo "0. Exit" + echo "==========================================" + read -p "Select option: " OPTION + + case $OPTION in + 1) + phase1_preparation + ;; + 2) + phase2_coordination_window + ;; + 3) + phase3_5_operator_coordination + ;; + 4) + phase6_bitcoin_confirmations + ;; + 5) + phase7_spv_proof + ;; + 6) + phase8_verification + ;; + 7) + check_operator_node_health + ;; + 8) + phase1_preparation + phase2_coordination_window + phase3_5_operator_coordination + log "⚠️ Pausing for operator coordination..." + log "After Bitcoin transaction is broadcast, continue with remaining phases." + read -p "Press Enter when Bitcoin transaction ID is available..." + phase6_bitcoin_confirmations + phase7_spv_proof + read -p "Press Enter when SPV proof has been submitted to Ethereum..." + phase8_verification + ;; + 0) + log "Exiting..." + exit 0 + ;; + *) + error "Invalid option" + ;; + esac + + log "" + log "Phase completed. Results saved to: $OUTPUT_DIR" +} + +# Run main function +main diff --git a/scripts/wallet-operator-mapper/operators.json b/scripts/wallet-operator-mapper/operators.json new file mode 100644 index 0000000000..fe4c1e3fb4 --- /dev/null +++ b/scripts/wallet-operator-mapper/operators.json @@ -0,0 +1,131 @@ +{ + "operators": { + "keep": [ + { + "provider": "STAKED", + "address": "0xf401aae8c639eb1638fd99b90eae8a4c54f9894d", + "status": "KEEP" + }, + { + "provider": "P2P", + "address": "0xb074a3b960f29a1448a2dd4de95210ca492c18d4", + "status": "KEEP" + }, + { + "provider": "BOAR", + "address": "0xffb804c2de78576ad011f68a7df63d739b8c8155", + "status": "KEEP" + }, + { + "provider": "NUCO", + "address": "0xd7f138ccf194ca2f49c28870b3b5f556b57fb8b7", + "status": "KEEP" + } + ], + "disable": [ + { + "provider": "STAKED", + "address": "0x7b251041b2445a3517db540d6fc9f7826c583922", + "status": "DISABLE" + }, + { + "provider": "STAKED", + "address": "0xf28b3e1decf9aa2c389685d27f699a6bc6f4cdab", + "status": "DISABLE" + }, + { + "provider": "STAKED", + "address": "0x35a78efe7a7531b4268df011bb2c5e002e20eb8b", + "status": "DISABLE" + }, + { + "provider": "STAKED", + "address": "0xabe527adbe374d38deabd5f540da44da971a09a1", + "status": "DISABLE" + }, + { + "provider": "STAKED", + "address": "0x4a1be85bf8eb73e748715bd40926349041427b98", + "status": "DISABLE" + }, + { + "provider": "P2P", + "address": "0xb102b1e8b61376d1d4f0bd8f6e4e4bb3cf79b75b", + "status": "DISABLE" + }, + { + "provider": "P2P", + "address": "0x8235b62a2b69b03d577b254d980437702238028c", + "status": "DISABLE" + }, + { + "provider": "P2P", + "address": "0x448386af932b9b433da640d88b9b9fc21af82e0d", + "status": "DISABLE" + }, + { + "provider": "P2P", + "address": "0xcffd10eb581f659dea37f7a1e8ca6c4193ae44d8", + "status": "DISABLE" + }, + { + "provider": "P2P", + "address": "0x87a185a640739482824fc718323d6df7ab1192f5", + "status": "DISABLE" + }, + { + "provider": "BOAR", + "address": "0x39dbe14b0b8d3f9d2fc36eb1f58e0a0959fab1ca", + "status": "DISABLE" + }, + { + "provider": "BOAR", + "address": "0xae58c689e5846aa1f18bf65e096a47fbca63b82e", + "status": "DISABLE" + }, + { + "provider": "BOAR", + "address": "0x65a4ebd101a7a8a762eb3f20371f5222e3490eee", + "status": "DISABLE" + }, + { + "provider": "BOAR", + "address": "0x909e8c501bc6cd135efb6878577dac89b95f34dc", + "status": "DISABLE" + }, + { + "provider": "BOAR", + "address": "0xff4cd368f6a9a69d3e082c26414c0f0666b8a02f", + "status": "DISABLE" + }, + { + "provider": "NUCO", + "address": "0xb6c7382f67c6866597ff3d8902220f1505bd6825", + "status": "DISABLE" + } + ] + }, + "summary": { + "totalOperators": 20, + "keep": 4, + "disable": 16, + "byProvider": { + "STAKED": { + "keep": 1, + "disable": 5 + }, + "P2P": { + "keep": 1, + "disable": 5 + }, + "BOAR": { + "keep": 1, + "disable": 5 + }, + "NUCO": { + "keep": 1, + "disable": 1 + } + } + } +} diff --git a/scripts/wallet-operator-mapper/package.json b/scripts/wallet-operator-mapper/package.json new file mode 100644 index 0000000000..dd4860a18d --- /dev/null +++ b/scripts/wallet-operator-mapper/package.json @@ -0,0 +1,26 @@ +{ + "name": "wallet-operator-mapper", + "version": "1.0.0", + "description": "Query Ethereum to map tBTC wallets to operators", + "main": "query-wallets.js", + "scripts": { + "query": "node query-wallets.js", + "test": "node query-wallets.js --dry-run" + }, + "keywords": [ + "tbtc", + "wallet", + "operator", + "ethereum", + "threshold" + ], + "author": "Leonardo Saturnino", + "license": "MIT", + "dependencies": { + "ethers": "^6.13.4", + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/scripts/wallet-operator-mapper/query-dkg-events.js b/scripts/wallet-operator-mapper/query-dkg-events.js new file mode 100644 index 0000000000..773f4f4b0e --- /dev/null +++ b/scripts/wallet-operator-mapper/query-dkg-events.js @@ -0,0 +1,413 @@ +#!/usr/bin/env node + +/** + * DKG Event Query Tool + * + * Queries WalletRegistry DkgResultSubmitted events to extract operator membership for each wallet. + * This is the canonical on-chain way to get wallet-to-operator mappings. + * + * Usage: node query-dkg-events.js [--limit N] + * Output: Updates wallet-operator-mapping.json with real operator data + */ + +const { ethers } = require('ethers'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +// Configuration +const CONFIG = { + rpcUrl: process.env.ETHEREUM_RPC_URL || 'https://ethereum.publicnode.com', + debug: process.env.DEBUG === 'true', + limitWallets: parseInt(process.env.LIMIT_WALLETS) || null +}; + +// Parse CLI args +const limitArg = process.argv.find(arg => arg.startsWith('--limit=')); +if (limitArg) { + CONFIG.limitWallets = parseInt(limitArg.split('=')[1]); +} + +// File paths +const PROOF_OF_FUNDS_PATH = '/Users/leonardosaturnino/Documents/GitHub/memory-bank/20250809-beta-staker-consolidation/knowledge/20251006-tbtc-proof-of-funds.json'; +const OPERATORS_PATH = path.join(__dirname, 'operators.json'); +const BRIDGE_CONTRACT_PATH = path.join(__dirname, 'contracts', 'Bridge.json'); +const OUTPUT_PATH = path.join(__dirname, 'wallet-operator-mapping.json'); + +// Contract addresses +const WALLET_REGISTRY_ADDRESS = '0x46d52E41C2F300BC82217Ce22b920c34995204eb'; +const SORTITION_POOL_ADDRESS = '0xc2731fb2823af3Efc2694c9bC86F444d5c5bb4Dc'; // ECDSA sortition pool (from WalletRegistry.sortitionPool()) + +// Logging utilities +const log = { + info: (msg) => console.log(`ℹ️ ${msg}`), + success: (msg) => console.log(`βœ… ${msg}`), + error: (msg) => console.error(`❌ ${msg}`), + debug: (msg) => CONFIG.debug && console.log(`πŸ” ${msg}`), + warn: (msg) => console.warn(`⚠️ ${msg}`) +}; + +// Load JSON file +function loadJSON(filePath) { + try { + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + const data = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(data); + } catch (error) { + log.error(`Failed to load ${filePath}: ${error.message}`); + throw error; + } +} + +/** + * Estimate block number from timestamp + * Uses simple linear approximation: ~12 seconds per block + */ +async function estimateBlockFromTimestamp(provider, targetTimestamp) { + const currentBlock = await provider.getBlock('latest'); + const currentTimestamp = currentBlock.timestamp; + + if (targetTimestamp >= currentTimestamp) { + return currentBlock.number; + } + + // Estimate: ~12 seconds per block on average + const secondsDiff = currentTimestamp - targetTimestamp; + const blocksDiff = Math.floor(secondsDiff / 12); + + return Math.max(0, currentBlock.number - blocksDiff); +} + +/** + * Query DKG events for a specific wallet + * Two-step lookup: WalletCreated (walletID β†’ dkgResultHash) β†’ DkgResultSubmitted (dkgResultHash β†’ members) + */ +async function getDkgEventForWallet(walletRegistry, ecdsaWalletID, createdAtTimestamp, provider) { + try { + // Convert timestamp to approximate block number + // Use wallet creation time to narrow search range (Β±20,000 blocks ~ Β±3 days) + const currentBlock = await provider.getBlockNumber(); + const estimatedBlock = await estimateBlockFromTimestamp(provider, createdAtTimestamp); + + const fromBlock = Math.max(0, estimatedBlock - 20000); + const toBlock = Math.min(currentBlock, estimatedBlock + 20000); + + log.debug(`Querying events for wallet ${ecdsaWalletID.slice(0, 10)}... (blocks ${fromBlock}-${toBlock})`); + + // Step 1: Query WalletCreated event to get dkgResultHash + const walletCreatedFilter = walletRegistry.filters.WalletCreated(ecdsaWalletID); + const walletCreatedEvents = await walletRegistry.queryFilter(walletCreatedFilter, fromBlock, toBlock); + + if (walletCreatedEvents.length === 0) { + log.warn(`No WalletCreated event found for ${ecdsaWalletID.slice(0, 10)}...`); + return null; + } + + const dkgResultHash = walletCreatedEvents[0].args.dkgResultHash; + log.debug(` Found dkgResultHash: ${dkgResultHash.slice(0, 10)}...`); + + // Step 2: Query DkgResultSubmitted event using dkgResultHash + // DKG event happens before WalletCreated, so search earlier blocks + const dkgFromBlock = Math.max(0, fromBlock - 10000); // Search 10k blocks earlier + const dkgResultFilter = walletRegistry.filters.DkgResultSubmitted(dkgResultHash); + const dkgEvents = await walletRegistry.queryFilter(dkgResultFilter, dkgFromBlock, toBlock); + + if (dkgEvents.length === 0) { + log.warn(`No DkgResultSubmitted event found for hash ${dkgResultHash.slice(0, 10)}...`); + return null; + } + + const event = dkgEvents[0]; + + // Parse event data + // event DkgResultSubmitted( + // uint256 indexed resultHash, + // bytes32 indexed seed, + // DkgResult result + // ) + // where DkgResult contains: submitterMemberIndex, groupPubKey, misbehavedMembersIndices, signatures, signingMembersIndices, members, membersHash + + const result = event.args.result; + const memberIds = result.members.map(id => parseInt(id.toString())); + + log.debug(` Found ${memberIds.length} members`); + + return { + blockNumber: event.blockNumber, + transactionHash: event.transactionHash, + memberIds: memberIds + }; + + } catch (error) { + log.error(`Failed to query DKG event for ${ecdsaWalletID.slice(0, 10)}...: ${error.message}`); + return null; + } +} + +/** + * Get operator address from sortition pool by member ID + */ +async function getOperatorAddress(sortitionPool, memberId) { + try { + const operator = await sortitionPool.getIDOperator(memberId); + return operator.toLowerCase(); + } catch (error) { + log.warn(`Failed to get operator for member ID ${memberId}: ${error.message}`); + return null; + } +} + +/** + * Map operator address to provider info + */ +function getOperatorInfo(address, operators) { + const normalizedAddr = address.toLowerCase(); + + // Check KEEP operators + const keepOp = operators.keep.find(op => op.address.toLowerCase() === normalizedAddr); + if (keepOp) { + return { ...keepOp, status: 'KEEP' }; + } + + // Check DISABLE operators + const disableOp = operators.disable.find(op => op.address.toLowerCase() === normalizedAddr); + if (disableOp) { + return { ...disableOp, status: 'DISABLE' }; + } + + return { + provider: 'UNKNOWN', + address: address, + status: 'UNKNOWN' + }; +} + +/** + * Main execution + */ +async function main() { + log.info('DKG Event Query Tool'); + log.info('===================\n'); + + // Load input data + log.info('Loading configuration files...'); + const proofOfFunds = loadJSON(PROOF_OF_FUNDS_PATH); + const operators = loadJSON(OPERATORS_PATH); + const bridgeContract = loadJSON(BRIDGE_CONTRACT_PATH); + + log.success(`Loaded ${proofOfFunds.wallets.length} wallets from proof-of-funds`); + log.success(`Loaded ${operators.operators.keep.length} KEEP + ${operators.operators.disable.length} DISABLE operators\n`); + + // Connect to Ethereum + log.info(`Connecting to Ethereum: ${CONFIG.rpcUrl}`); + const provider = new ethers.JsonRpcProvider(CONFIG.rpcUrl); + + // Initialize contracts + const bridge = new ethers.Contract( + bridgeContract.contractAddress, + bridgeContract.abi, + provider + ); + + // WalletRegistry ABI for DKG events + // Correct struct from EcdsaDkg.sol: + // struct Result { uint256 submitterMemberIndex; bytes groupPubKey; uint8[] misbehavedMembersIndices; bytes signatures; uint256[] signingMembersIndices; uint32[] members; bytes32 membersHash; } + const walletRegistryAbi = [ + 'event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, tuple(uint256 submitterMemberIndex, bytes groupPubKey, uint8[] misbehavedMembersIndices, bytes signatures, uint256[] signingMembersIndices, uint32[] members, bytes32 membersHash) result)', + 'event WalletCreated(bytes32 indexed walletID, bytes32 indexed dkgResultHash)', + 'function getWallet(bytes32 walletID) external view returns (tuple(bytes32 membersIdsHash, bytes32 publicKeyX, bytes32 publicKeyY))' + ]; + + const walletRegistry = new ethers.Contract( + WALLET_REGISTRY_ADDRESS, + walletRegistryAbi, + provider + ); + + // Sortition Pool ABI + const sortitionPoolAbi = [ + 'function getIDOperator(uint32) external view returns (address)' + ]; + + const sortitionPool = new ethers.Contract( + SORTITION_POOL_ADDRESS, + sortitionPoolAbi, + provider + ); + + // Verify connection + try { + const network = await provider.getNetwork(); + log.success(`Connected to network: ${network.name} (chainId: ${network.chainId})`); + log.success(`Bridge contract: ${bridgeContract.contractAddress}`); + log.success(`WalletRegistry contract: ${WALLET_REGISTRY_ADDRESS}`); + log.success(`SortitionPool contract: ${SORTITION_POOL_ADDRESS}\n`); + } catch (error) { + log.error(`Failed to connect to Ethereum: ${error.message}`); + process.exit(1); + } + + // Determine wallets to query + const walletsToQuery = CONFIG.limitWallets + ? proofOfFunds.wallets.slice(0, CONFIG.limitWallets) + : proofOfFunds.wallets; + + if (CONFIG.limitWallets) { + log.warn(`LIMIT MODE - Processing first ${CONFIG.limitWallets} wallets only\n`); + } + + log.info(`Querying wallet data from Bridge + DKG events...`); + log.info(`This may take 2-3 minutes for all wallets...\n`); + + const walletData = []; + const startTime = Date.now(); + + for (let i = 0; i < walletsToQuery.length; i++) { + const wallet = walletsToQuery[i]; + const progress = `[${i + 1}/${walletsToQuery.length}]`; + + log.info(`${progress} ${wallet.walletPublicKeyHash.slice(0, 10)}...`); + + try { + // Step 1: Get ecdsaWalletID from Bridge + const bridgeWallet = await bridge.wallets(wallet.walletPublicKeyHash); + const ecdsaWalletID = bridgeWallet.ecdsaWalletID; + const createdAt = parseInt(bridgeWallet.createdAt.toString()); + + if (ecdsaWalletID === ethers.ZeroHash) { + log.warn(` Wallet has no ecdsaWalletID (not created via DKG)`); + walletData.push({ + walletPKH: wallet.walletPublicKeyHash, + btcBalance: parseFloat(wallet.walletBitcoinBalance), + ecdsaWalletID: ethers.ZeroHash, + state: 'Unknown', + memberCount: 0, + operators: [] + }); + continue; + } + + // Step 2: Query DKG event for this wallet + const dkgEvent = await getDkgEventForWallet(walletRegistry, ecdsaWalletID, createdAt, provider); + + if (!dkgEvent) { + log.warn(` No DKG event found`); + walletData.push({ + walletPKH: wallet.walletPublicKeyHash, + btcBalance: parseFloat(wallet.walletBitcoinBalance), + ecdsaWalletID: ecdsaWalletID, + state: 'Live', + memberCount: 0, + operators: [] + }); + continue; + } + + // Step 3: Resolve member IDs to operator addresses + const operatorAddresses = []; + + for (const memberId of dkgEvent.memberIds) { + const address = await getOperatorAddress(sortitionPool, memberId); + if (address) { + operatorAddresses.push(address); + } + } + + // Step 4: Map operators to provider info + const operatorInfos = operatorAddresses.map(addr => getOperatorInfo(addr, operators.operators)); + + const hasDeprecated = operatorInfos.some(op => op.status === 'DISABLE'); + const hasActive = operatorInfos.some(op => op.status === 'KEEP'); + + log.success(` Found ${operatorInfos.length} operators (${hasDeprecated ? 'HAS DEPRECATED' : 'no deprecated'})`); + + walletData.push({ + walletPKH: wallet.walletPublicKeyHash, + btcBalance: parseFloat(wallet.walletBitcoinBalance), + ecdsaWalletID: ecdsaWalletID, + state: 'Live', // Simplified - we know from earlier queries + memberCount: operatorInfos.length, + memberIds: dkgEvent.memberIds, + operators: operatorInfos, + hasDeprecatedOperator: hasDeprecated, + hasActiveOperator: hasActive, + needsSweep: hasDeprecated + }); + + } catch (error) { + log.error(` Failed: ${error.message}`); + } + } + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + log.success(`\nCompleted in ${elapsed}s\n`); + + // Generate summary + log.info('Generating summary...'); + + const summary = { + totalWallets: walletData.length, + totalBTC: walletData.reduce((sum, w) => sum + w.btcBalance, 0).toFixed(8), + byProvider: { + STAKED: { wallets: 0, btc: 0 }, + P2P: { wallets: 0, btc: 0 }, + BOAR: { wallets: 0, btc: 0 }, + NUCO: { wallets: 0, btc: 0 } + } + }; + + // Calculate per-provider totals + walletData.forEach(wallet => { + if (wallet.hasDeprecatedOperator) { + const deprecatedOps = wallet.operators.filter(op => op.status === 'DISABLE'); + const providers = new Set(deprecatedOps.map(op => op.provider)); + + providers.forEach(provider => { + if (summary.byProvider[provider]) { + summary.byProvider[provider].wallets++; + summary.byProvider[provider].btc += wallet.btcBalance; + } + }); + } + }); + + // Round BTC + Object.keys(summary.byProvider).forEach(provider => { + summary.byProvider[provider].btc = parseFloat(summary.byProvider[provider].btc.toFixed(8)); + }); + + // Save output + const output = { + timestamp: new Date().toISOString(), + summary, + wallets: walletData + }; + + fs.writeFileSync(OUTPUT_PATH, JSON.stringify(output, null, 2)); + log.success(`Results saved to ${OUTPUT_PATH}\n`); + + // Print summary + console.log('='.repeat(80)); + console.log('SUMMARY'); + console.log('='.repeat(80)); + console.log(`\nTotal Wallets: ${summary.totalWallets}`); + console.log(`Total BTC: ${summary.totalBTC} BTC\n`); + + console.log('BTC to Sweep by Provider:'); + Object.entries(summary.byProvider).forEach(([provider, stats]) => { + if (stats.wallets > 0) { + console.log(` ${provider}: ${stats.wallets} wallets, ${stats.btc} BTC`); + } + }); + + console.log('\n' + '='.repeat(80)); +} + +// Run +main().catch(error => { + log.error(`Fatal error: ${error.message}`); + console.error(error); + process.exit(1); +}); diff --git a/scripts/wallet-operator-mapper/validate-operator-list.js b/scripts/wallet-operator-mapper/validate-operator-list.js new file mode 100644 index 0000000000..8a19bc1ea6 --- /dev/null +++ b/scripts/wallet-operator-mapper/validate-operator-list.js @@ -0,0 +1,251 @@ +#!/usr/bin/env node + +/** + * Validate Operator List Completeness + * + * Compare CSV operators with on-chain discovered operators + */ + +const fs = require('fs'); +const path = require('path'); + +const MAPPING_FILE = path.join(__dirname, 'wallet-operator-mapping.json'); +const CSV_PATH = '/Users/leonardosaturnino/Documents/GitHub/memory-bank/20250809-beta-staker-consolidation/knowledge/threshold_stakers_may_2025.csv'; + +console.log('πŸ” Operator List Validation\n'); +console.log('='.repeat(80)); + +// Load on-chain data +const mappingData = JSON.parse(fs.readFileSync(MAPPING_FILE)); + +// Extract all unique operators from on-chain data +const onChainOperators = new Set(); +mappingData.wallets.forEach(wallet => { + if (wallet.operators) { + wallet.operators.forEach(op => { + onChainOperators.add(op.address.toLowerCase()); + }); + } +}); + +console.log(`\nOperators found on-chain: ${onChainOperators.size}`); + +// Parse CSV +const csvData = fs.readFileSync(CSV_PATH, 'utf8'); +const csvLines = csvData.split('\n').slice(1).filter(line => line.trim() && !line.startsWith('Total')); + +const csvOperators = new Set(); +const csvOperatorDetails = []; + +csvLines.forEach(line => { + const parts = line.split(','); + if (parts.length >= 3 && parts[2]) { + const operatorAddr = parts[2].trim().toLowerCase(); + if (operatorAddr && operatorAddr.startsWith('0x')) { + csvOperators.add(operatorAddr); + csvOperatorDetails.push({ + identification: parts[0]?.trim(), + stakingProvider: parts[1]?.trim(), + operatorAddress: operatorAddr, + stake: parts[4]?.trim() + }); + } + } +}); + +console.log(`Operators in CSV: ${csvOperators.size}\n`); + +// Compare +console.log('='.repeat(80)); +console.log('COMPARISON'); +console.log('='.repeat(80)); + +// Operators in CSV but NOT found on-chain +const csvOnly = Array.from(csvOperators).filter(addr => !onChainOperators.has(addr)); + +console.log(`\n❌ Operators in CSV but NOT found on-chain: ${csvOnly.length}`); +if (csvOnly.length > 0) { + csvOnly.forEach(addr => { + const detail = csvOperatorDetails.find(d => d.operatorAddress === addr); + console.log(` - ${addr} (${detail?.identification || 'Unknown'})`); + }); +} + +// Operators found on-chain but NOT in CSV +const onChainOnly = Array.from(onChainOperators).filter(addr => !csvOperators.has(addr)); + +console.log(`\n⚠️ Operators found on-chain but NOT in CSV: ${onChainOnly.length}`); +if (onChainOnly.length > 0) { + onChainOnly.forEach(addr => { + // Try to find this operator in our mapping data + let foundInWallets = 0; + let totalShare = 0; + + mappingData.wallets.forEach(wallet => { + if (wallet.operators) { + const op = wallet.operators.find(o => o.address.toLowerCase() === addr); + if (op) { + foundInWallets++; + totalShare += wallet.btcBalance / wallet.memberCount; + } + } + }); + + console.log(` - ${addr}`); + console.log(` Found in ${foundInWallets} wallets`); + console.log(` Estimated BTC share: ${totalShare.toFixed(2)} BTC`); + }); +} + +// Operators in BOTH CSV and on-chain +const commonOperators = Array.from(csvOperators).filter(addr => onChainOperators.has(addr)); + +console.log(`\nβœ… Operators in BOTH CSV and on-chain: ${commonOperators.length}`); + +// Calculate total BTC for ALL operators +console.log('\n' + '='.repeat(80)); +console.log('ALL OPERATORS BTC ANALYSIS'); +console.log('='.repeat(80)); + +const allOperatorShares = {}; + +// Initialize with CSV operators +csvOperatorDetails.forEach(detail => { + allOperatorShares[detail.operatorAddress] = { + identification: detail.identification, + stakingProvider: detail.stakingProvider, + inCSV: true, + btcShare: 0, + walletCount: 0, + status: 'UNKNOWN' + }; +}); + +// Add on-chain only operators +onChainOnly.forEach(addr => { + allOperatorShares[addr] = { + identification: 'Not in CSV', + stakingProvider: 'Unknown', + inCSV: false, + btcShare: 0, + walletCount: 0, + status: 'UNKNOWN' + }; +}); + +// Calculate BTC shares from on-chain data +mappingData.wallets.forEach(wallet => { + if (wallet.operators && wallet.memberCount > 0) { + const sharePerOp = wallet.btcBalance / wallet.memberCount; + + wallet.operators.forEach(op => { + const addr = op.address.toLowerCase(); + if (!allOperatorShares[addr]) { + allOperatorShares[addr] = { + identification: 'Found on-chain', + stakingProvider: op.provider || 'Unknown', + inCSV: false, + btcShare: 0, + walletCount: 0, + status: op.status || 'UNKNOWN' + }; + } + + allOperatorShares[addr].btcShare += sharePerOp; + allOperatorShares[addr].walletCount++; + allOperatorShares[addr].status = op.status || allOperatorShares[addr].status; + }); + } +}); + +// Sort by BTC share +const sortedOperators = Object.entries(allOperatorShares) + .map(([addr, data]) => ({ address: addr, ...data })) + .sort((a, b) => b.btcShare - a.btcShare); + +// Summary by category +console.log('\nBTC Distribution by Operator Type:\n'); + +const csvOpsTotal = sortedOperators + .filter(op => op.inCSV && op.btcShare > 0) + .reduce((sum, op) => sum + op.btcShare, 0); + +const nonCsvOpsTotal = sortedOperators + .filter(op => !op.inCSV && op.btcShare > 0) + .reduce((sum, op) => sum + op.btcShare, 0); + +const csvOpsActive = sortedOperators + .filter(op => op.inCSV && op.btcShare > 0); + +const nonCsvOpsActive = sortedOperators + .filter(op => !op.inCSV && op.btcShare > 0); + +console.log(`CSV Operators (active on-chain): ${csvOpsActive.length}`); +console.log(` Total BTC share: ${csvOpsTotal.toFixed(2)} BTC`); +console.log(` Percentage: ${(csvOpsTotal / 5923.91 * 100).toFixed(2)}%`); + +console.log(`\nNon-CSV Operators (active on-chain): ${nonCsvOpsActive.length}`); +console.log(` Total BTC share: ${nonCsvOpsTotal.toFixed(2)} BTC`); +console.log(` Percentage: ${(nonCsvOpsTotal / 5923.91 * 100).toFixed(2)}%`); + +console.log(`\nTotal BTC accounted for: ${(csvOpsTotal + nonCsvOpsTotal).toFixed(2)} BTC`); + +// Top 50 operators by BTC +console.log('\n' + '='.repeat(80)); +console.log('TOP 50 OPERATORS BY BTC SHARE'); +console.log('='.repeat(80)); + +console.log('\nRank | Address (short) | Identification | In CSV? | BTC Share | Wallets | Status'); +console.log('-'.repeat(100)); + +sortedOperators.slice(0, 50).forEach((op, i) => { + const addrShort = op.address.slice(0, 10) + '...' + op.address.slice(-6); + const idShort = (op.identification || 'Unknown').substring(0, 20).padEnd(20); + const inCsv = op.inCSV ? 'βœ…' : '❌'; + const btc = op.btcShare.toFixed(2).padStart(8); + const wallets = op.walletCount.toString().padStart(3); + + console.log(`${(i+1).toString().padStart(4)} | ${addrShort} | ${idShort} | ${inCsv} | ${btc} BTC | ${wallets} | ${op.status}`); +}); + +// Check completeness +console.log('\n' + '='.repeat(80)); +console.log('COMPLETENESS ASSESSMENT'); +console.log('='.repeat(80)); + +const csvMissingOperators = csvOperators.size - commonOperators.length; +const onChainMissingFromCsv = onChainOperators.size - commonOperators.length; + +console.log(`\n CSV completeness: ${commonOperators.length}/${csvOperators.size} operators found on-chain (${((commonOperators.length/csvOperators.size)*100).toFixed(1)}%)`); +console.log(`On-chain coverage: ${commonOperators.length}/${onChainOperators.size} operators in CSV (${((commonOperators.length/onChainOperators.size)*100).toFixed(1)}%)`); + +if (csvMissingOperators > 0) { + console.log(`\n⚠️ ${csvMissingOperators} CSV operators NOT found on-chain (may be inactive or never participated in DKG)`); +} + +if (onChainMissingFromCsv > 0) { + console.log(`\n⚠️ ${onChainMissingFromCsv} active operators NOT in CSV (missing from documentation!)`); +} + +if (csvMissingOperators === 0 && onChainMissingFromCsv === 0) { + console.log('\nβœ… CSV is COMPLETE - all operators documented'); +} else { + console.log('\n❌ CSV is INCOMPLETE - missing operators or contains inactive ones'); +} + +// Final summary +console.log('\n' + '='.repeat(80)); +console.log('SUMMARY'); +console.log('='.repeat(80)); + +console.log(`\nTotal unique operators in system: ${sortedOperators.filter(op => op.btcShare > 0).length}`); +console.log(`Total BTC in analyzed wallets: 5,923.91 BTC`); +console.log(`\nOperators by status:`); + +const keepOps = sortedOperators.filter(op => op.status === 'KEEP' && op.btcShare > 0); +const disableOps = sortedOperators.filter(op => op.status === 'DISABLE' && op.btcShare > 0); +const unknownOps = sortedOperators.filter(op => op.status === 'UNKNOWN' && op.btcShare > 0); + +console.log(` KEEP (active): ${keepOps.length} operators, ${keepOps.reduce((s, op) => s + op.btcShare, 0).toFixed(2)} BTC`); +console.log(` DISABLE (deprecated): ${disableOps.length} operators, ${disableOps.reduce((s, op) => s + op.btcShare, 0).toFixed(2)} BTC`); +console.log(` UNKNOWN: ${unknownOps.length} operators, ${unknownOps.reduce((s, op) => s + op.btcShare, 0).toFixed(2)} BTC`); From 6461451ca823540eceb6d2ee5bd3d40fe291ccd3 Mon Sep 17 00:00:00 2001 From: Leonardo Saturnino Date: Wed, 29 Oct 2025 14:05:05 -0400 Subject: [PATCH 2/2] fix(scripts): address wallet-operator-mapper review feedback Fix critical bugs and configuration issues identified in code review: Bug Fixes: - Fix wallet count aggregation in analyze-per-operator.js - Changed assignment operator from = to += on lines 122, 125 - Now correctly sums wallet counts across all operators per provider - Previously only recorded last operator's count, causing incorrect analysis Configuration: - Externalize hard-coded file paths to environment variables - query-dkg-events.js: Use PROOF_OF_FUNDS_PATH with ./data/ fallback - validate-operator-list.js: Use THRESHOLD_STAKERS_CSV_PATH with ./data/ fallback - Scripts now portable across team members and environments Documentation: - Add data file setup section to README.md - Document two setup options: default directory and env variables - Create data/README.md with file requirements and sources - Update .env.example with new configuration variables - Add data/ directory to .gitignore All review comments from piotr-roslaniec addressed. --- scripts/wallet-operator-mapper/.env.example | 7 ++++ scripts/wallet-operator-mapper/.gitignore | 3 ++ scripts/wallet-operator-mapper/README.md | 36 ++++++++++++++++++- .../analyze-per-operator.js | 4 +-- .../query-dkg-events.js | 3 +- .../validate-operator-list.js | 3 +- 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/scripts/wallet-operator-mapper/.env.example b/scripts/wallet-operator-mapper/.env.example index df0013d849..fe22583589 100644 --- a/scripts/wallet-operator-mapper/.env.example +++ b/scripts/wallet-operator-mapper/.env.example @@ -11,3 +11,10 @@ QUERY_DELAY_MS=100 # Optional: Enable verbose logging DEBUG=false + +# Data file paths (optional - defaults to ./data/ directory) +# Path to tBTC proof-of-funds JSON file +PROOF_OF_FUNDS_PATH=./data/tbtc-proof-of-funds.json + +# Path to threshold stakers CSV file +THRESHOLD_STAKERS_CSV_PATH=./data/threshold_stakers_may_2025.csv diff --git a/scripts/wallet-operator-mapper/.gitignore b/scripts/wallet-operator-mapper/.gitignore index d78a783a19..483ba5ac4c 100644 --- a/scripts/wallet-operator-mapper/.gitignore +++ b/scripts/wallet-operator-mapper/.gitignore @@ -8,6 +8,9 @@ package-lock.json # Output wallet-operator-mapping.json +# Data files +data/ + # Logs *.log npm-debug.log* diff --git a/scripts/wallet-operator-mapper/README.md b/scripts/wallet-operator-mapper/README.md index f3dc167f7e..58d4eb1427 100644 --- a/scripts/wallet-operator-mapper/README.md +++ b/scripts/wallet-operator-mapper/README.md @@ -12,10 +12,44 @@ npm install cp .env.example .env # Edit .env with your Alchemy/Infura archive node URL -# 3. Run analysis +# 3. Set up data files (see Data Files Setup below) +mkdir -p data +# Copy required data files to ./data/ directory + +# 4. Run analysis node analyze-per-operator.js ``` +## Data Files Setup + +The scripts require two data files from the Memory Bank project: + +### Required Files + +1. **tBTC Proof of Funds** (`tbtc-proof-of-funds.json`) + - Contains wallet balances and metadata + - Source: Memory Bank `/knowledge/20251006-tbtc-proof-of-funds.json` + +2. **Threshold Stakers CSV** (`threshold_stakers_may_2025.csv`) + - Contains operator list and stake information + - Source: Memory Bank `/knowledge/threshold_stakers_may_2025.csv` + +### Setup Options + +**Option 1: Use default data directory** (recommended) +```bash +mkdir -p data +cp /path/to/memory-bank/knowledge/20251006-tbtc-proof-of-funds.json data/tbtc-proof-of-funds.json +cp /path/to/memory-bank/knowledge/threshold_stakers_may_2025.csv data/threshold_stakers_may_2025.csv +``` + +**Option 2: Use environment variables** +```bash +# Add to .env file: +PROOF_OF_FUNDS_PATH=/custom/path/to/tbtc-proof-of-funds.json +THRESHOLD_STAKERS_CSV_PATH=/custom/path/to/threshold_stakers_may_2025.csv +``` + ## What This Does Analyzes tBTC wallets to identify which contain deprecated operators being removed during consolidation. diff --git a/scripts/wallet-operator-mapper/analyze-per-operator.js b/scripts/wallet-operator-mapper/analyze-per-operator.js index fa0ec1df44..12a575ed9e 100644 --- a/scripts/wallet-operator-mapper/analyze-per-operator.js +++ b/scripts/wallet-operator-mapper/analyze-per-operator.js @@ -119,10 +119,10 @@ Object.entries(operatorShares).forEach(([addr, data]) => { if (providerShares[data.provider]) { if (data.status === 'KEEP') { providerShares[data.provider].keep += data.totalShare; - providerShares[data.provider].keepWallets = data.walletCount; + providerShares[data.provider].keepWallets += data.walletCount; } else { providerShares[data.provider].disable += data.totalShare; - providerShares[data.provider].disableWallets = data.walletCount; + providerShares[data.provider].disableWallets += data.walletCount; } } }); diff --git a/scripts/wallet-operator-mapper/query-dkg-events.js b/scripts/wallet-operator-mapper/query-dkg-events.js index 773f4f4b0e..00a5742289 100644 --- a/scripts/wallet-operator-mapper/query-dkg-events.js +++ b/scripts/wallet-operator-mapper/query-dkg-events.js @@ -29,7 +29,8 @@ if (limitArg) { } // File paths -const PROOF_OF_FUNDS_PATH = '/Users/leonardosaturnino/Documents/GitHub/memory-bank/20250809-beta-staker-consolidation/knowledge/20251006-tbtc-proof-of-funds.json'; +const PROOF_OF_FUNDS_PATH = process.env.PROOF_OF_FUNDS_PATH || + path.join(__dirname, 'data', 'tbtc-proof-of-funds.json'); const OPERATORS_PATH = path.join(__dirname, 'operators.json'); const BRIDGE_CONTRACT_PATH = path.join(__dirname, 'contracts', 'Bridge.json'); const OUTPUT_PATH = path.join(__dirname, 'wallet-operator-mapping.json'); diff --git a/scripts/wallet-operator-mapper/validate-operator-list.js b/scripts/wallet-operator-mapper/validate-operator-list.js index 8a19bc1ea6..5b920d72ed 100644 --- a/scripts/wallet-operator-mapper/validate-operator-list.js +++ b/scripts/wallet-operator-mapper/validate-operator-list.js @@ -10,7 +10,8 @@ const fs = require('fs'); const path = require('path'); const MAPPING_FILE = path.join(__dirname, 'wallet-operator-mapping.json'); -const CSV_PATH = '/Users/leonardosaturnino/Documents/GitHub/memory-bank/20250809-beta-staker-consolidation/knowledge/threshold_stakers_may_2025.csv'; +const CSV_PATH = process.env.THRESHOLD_STAKERS_CSV_PATH || + path.join(__dirname, 'data', 'threshold_stakers_may_2025.csv'); console.log('πŸ” Operator List Validation\n'); console.log('='.repeat(80));