Skip to content

Commit efceeb2

Browse files
committed
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)
1 parent f85846f commit efceeb2

14 files changed

+5567
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Ethereum RPC Endpoint
2+
# Get free endpoints from:
3+
# - Infura: https://infura.io
4+
# - Alchemy: https://alchemy.com
5+
# - Public: https://ethereum.publicnode.com (rate limited)
6+
7+
ETHEREUM_RPC_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
8+
9+
# Optional: Query delay to avoid rate limiting (milliseconds)
10+
QUERY_DELAY_MS=100
11+
12+
# Optional: Enable verbose logging
13+
DEBUG=false
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Environment
2+
.env
3+
4+
# Dependencies
5+
node_modules/
6+
package-lock.json
7+
8+
# Output
9+
wallet-operator-mapping.json
10+
11+
# Logs
12+
*.log
13+
npm-debug.log*
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Wallet-Operator Mapper
2+
3+
**Purpose**: Maps tBTC v2 wallets to their controlling operators for beta staker consolidation.
4+
5+
## Quick Start
6+
7+
```bash
8+
# 1. Install dependencies
9+
npm install
10+
11+
# 2. Configure RPC endpoint
12+
cp .env.example .env
13+
# Edit .env with your Alchemy/Infura archive node URL
14+
15+
# 3. Run analysis
16+
node analyze-per-operator.js
17+
```
18+
19+
## What This Does
20+
21+
Analyzes tBTC wallets to identify which contain deprecated operators being removed during consolidation.
22+
23+
**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`.
24+
25+
## Main Scripts
26+
27+
### query-dkg-events.js
28+
Queries on-chain DKG events to extract wallet operator membership.
29+
- **Requirements**: Archive node RPC (Alchemy recommended)
30+
- **Runtime**: ~20 seconds per wallet
31+
- **Output**: `wallet-operator-mapping.json`
32+
- **Usage**: Run when wallet data needs updating
33+
34+
### analyze-per-operator.js
35+
Calculates BTC distribution by provider from mapping data.
36+
- **Runtime**: <1 second
37+
- **Output**: Console report with per-provider BTC analysis
38+
- **Usage**: Run after query-dkg-events.js to analyze results
39+
40+
### validate-operator-list.js
41+
Verifies operator list completeness against CSV data.
42+
- **Purpose**: Data quality checks
43+
- **Usage**: Optional validation step
44+
45+
## Configuration Files
46+
47+
### operators.json ⭐ CRITICAL
48+
Defines which operators to keep vs disable during consolidation.
49+
50+
**Structure**:
51+
- `operators.keep[]`: 4 active operators (1 per provider: STAKED, P2P, BOAR, NUCO)
52+
- `operators.disable[]`: 16 deprecated operators being removed
53+
54+
**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.
55+
56+
**Source**: Memory Bank `/knowledge/8-final-operator-consolidation-list.md`
57+
58+
### .env
59+
RPC endpoint configuration. Archive node required for historical DKG event queries.
60+
61+
## Output Data
62+
63+
**wallet-operator-mapping.json** contains:
64+
- Wallet metadata (PKH, BTC balance, state)
65+
- Complete operator membership (100 operators per wallet)
66+
- Operator addresses matched to providers
67+
- KEEP/DISABLE status per operator
68+
- Summary statistics by provider
69+
70+
## Integration
71+
72+
**Part of**: Beta Staker Consolidation (Memory Bank: `/memory-bank/20250809-beta-staker-consolidation/`)
73+
74+
**Used for**:
75+
- Monitoring dashboard data source (load mapping into Prometheus)
76+
- Draining progress assessment (identify wallets requiring manual sweeps)
77+
- Operator removal validation (verify wallets empty before removal)
78+
79+
## Important Notes
80+
81+
- **Equal-split calculation** is for analysis only—operators hold cryptographic key shares, not BTC shares
82+
- All wallets require 51/100 threshold signatures for transactions
83+
- Manual sweeps need coordination from all 4 providers simultaneously
84+
- Deprecated operators cannot be removed until their wallets reach 0 BTC
85+
86+
## Documentation
87+
88+
- `docs/` - Manual sweep procedures and technical processes
89+
- See Memory Bank for complete consolidation planning and correlation analysis
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Proper Per-Operator BTC Analysis
5+
*
6+
* Calculates actual BTC share per operator/provider
7+
*/
8+
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const MAPPING_FILE = path.join(__dirname, 'wallet-operator-mapping.json');
13+
const OPERATORS_FILE = path.join(__dirname, 'operators.json');
14+
15+
const data = JSON.parse(fs.readFileSync(MAPPING_FILE));
16+
const operatorsConfig = JSON.parse(fs.readFileSync(OPERATORS_FILE));
17+
18+
console.log('🔍 Proper BTC Analysis by Operator/Provider\n');
19+
console.log('='.repeat(80));
20+
21+
// Get wallets with operator data
22+
const walletsWithOps = data.wallets.filter(w => w.memberCount > 0);
23+
24+
console.log(`\nTotal wallets with operator data: ${walletsWithOps.length}`);
25+
console.log(`Total BTC in these wallets: ${walletsWithOps.reduce((s, w) => s + w.btcBalance, 0).toFixed(8)} BTC`);
26+
27+
// Method 1: Per-wallet breakdown showing which providers are involved
28+
console.log('\n' + '='.repeat(80));
29+
console.log('METHOD 1: Per-Wallet Provider Involvement');
30+
console.log('='.repeat(80));
31+
32+
const walletBreakdown = walletsWithOps.map(wallet => {
33+
const deprecatedOps = wallet.operators.filter(op => op.status === 'DISABLE');
34+
const providers = new Set(deprecatedOps.map(op => op.provider));
35+
36+
return {
37+
walletPKH: wallet.walletPKH,
38+
btcBalance: wallet.btcBalance,
39+
totalOperators: wallet.memberCount,
40+
deprecatedCount: deprecatedOps.length,
41+
providersInvolved: Array.from(providers).sort(),
42+
activeOperators: wallet.operators.filter(op => op.status === 'KEEP').length
43+
};
44+
});
45+
46+
// Group by provider combination
47+
const providerGroups = {};
48+
walletBreakdown.forEach(w => {
49+
const key = w.providersInvolved.join('+');
50+
if (!providerGroups[key]) {
51+
providerGroups[key] = {
52+
providers: w.providersInvolved,
53+
wallets: [],
54+
totalBTC: 0
55+
};
56+
}
57+
providerGroups[key].wallets.push(w);
58+
providerGroups[key].totalBTC += w.btcBalance;
59+
});
60+
61+
console.log('\nWallets grouped by provider involvement:\n');
62+
Object.entries(providerGroups).forEach(([key, group]) => {
63+
console.log(`Providers: ${group.providers.join(', ') || 'None'}`);
64+
console.log(` Wallets: ${group.wallets.length}`);
65+
console.log(` Total BTC: ${group.totalBTC.toFixed(8)} BTC`);
66+
console.log(` Average BTC per wallet: ${(group.totalBTC / group.wallets.length).toFixed(2)} BTC`);
67+
console.log();
68+
});
69+
70+
// Method 2: Equal-split calculation (BTC / operators in wallet)
71+
console.log('='.repeat(80));
72+
console.log('METHOD 2: Equal-Split Per-Operator Share');
73+
console.log('='.repeat(80));
74+
console.log('\nAssumption: BTC is split equally among all 100 operators in each wallet\n');
75+
76+
const operatorShares = {};
77+
78+
// Initialize
79+
operatorsConfig.operators.keep.forEach(op => {
80+
operatorShares[op.address.toLowerCase()] = {
81+
provider: op.provider,
82+
status: 'KEEP',
83+
totalShare: 0,
84+
walletCount: 0
85+
};
86+
});
87+
88+
operatorsConfig.operators.disable.forEach(op => {
89+
operatorShares[op.address.toLowerCase()] = {
90+
provider: op.provider,
91+
status: 'DISABLE',
92+
totalShare: 0,
93+
walletCount: 0
94+
};
95+
});
96+
97+
// Calculate shares
98+
walletsWithOps.forEach(wallet => {
99+
const sharePerOperator = wallet.btcBalance / wallet.memberCount;
100+
101+
wallet.operators.forEach(op => {
102+
const addr = op.address.toLowerCase();
103+
if (operatorShares[addr]) {
104+
operatorShares[addr].totalShare += sharePerOperator;
105+
operatorShares[addr].walletCount++;
106+
}
107+
});
108+
});
109+
110+
// Group by provider
111+
const providerShares = {
112+
STAKED: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 },
113+
P2P: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 },
114+
BOAR: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 },
115+
NUCO: { keep: 0, disable: 0, keepWallets: 0, disableWallets: 0 }
116+
};
117+
118+
Object.entries(operatorShares).forEach(([addr, data]) => {
119+
if (providerShares[data.provider]) {
120+
if (data.status === 'KEEP') {
121+
providerShares[data.provider].keep += data.totalShare;
122+
providerShares[data.provider].keepWallets = data.walletCount;
123+
} else {
124+
providerShares[data.provider].disable += data.totalShare;
125+
providerShares[data.provider].disableWallets = data.walletCount;
126+
}
127+
}
128+
});
129+
130+
console.log('Per-Provider BTC Shares (Equal-Split Method):\n');
131+
console.log('Provider | KEEP Ops Share | DISABLE Ops Share | Total Share');
132+
console.log('-'.repeat(80));
133+
134+
Object.entries(providerShares).forEach(([provider, shares]) => {
135+
const total = shares.keep + shares.disable;
136+
console.log(`${provider.padEnd(8)} | ${shares.keep.toFixed(2).padStart(13)} BTC | ${shares.disable.toFixed(2).padStart(17)} BTC | ${total.toFixed(2)} BTC`);
137+
});
138+
139+
console.log('\n' + '='.repeat(80));
140+
console.log('METHOD 3: Deprecated Operator BTC (What needs to be "moved")');
141+
console.log('='.repeat(80));
142+
console.log('\nThis shows the BTC share held by deprecated operators that');
143+
console.log('needs to remain accessible during the draining period.\n');
144+
145+
Object.entries(providerShares).forEach(([provider, shares]) => {
146+
console.log(`${provider}:`);
147+
console.log(` Deprecated operator share: ${shares.disable.toFixed(2)} BTC`);
148+
console.log(` Number of wallets involved: ${shares.disableWallets}`);
149+
console.log(` Average per wallet: ${shares.disableWallets > 0 ? (shares.disable / shares.disableWallets).toFixed(2) : 0} BTC`);
150+
console.log();
151+
});
152+
153+
// Method 4: Detailed operator-by-operator breakdown
154+
console.log('='.repeat(80));
155+
console.log('METHOD 4: Individual Operator Breakdown (Top 20 by BTC)');
156+
console.log('='.repeat(80));
157+
158+
const operatorList = Object.entries(operatorShares)
159+
.map(([addr, data]) => ({
160+
address: addr,
161+
...data
162+
}))
163+
.sort((a, b) => b.totalShare - a.totalShare)
164+
.slice(0, 20);
165+
166+
console.log('\nOperator Address | Provider | Status | BTC Share | Wallets');
167+
console.log('-'.repeat(80));
168+
operatorList.forEach(op => {
169+
const addrShort = op.address.slice(0, 10) + '...' + op.address.slice(-6);
170+
console.log(`${addrShort} | ${op.provider.padEnd(7)} | ${op.status.padEnd(7)} | ${op.totalShare.toFixed(2).padStart(8)} BTC | ${op.walletCount}`);
171+
});
172+
173+
// Summary
174+
console.log('\n' + '='.repeat(80));
175+
console.log('SUMMARY & INTERPRETATION');
176+
console.log('='.repeat(80));
177+
178+
const totalBTC = walletsWithOps.reduce((s, w) => s + w.btcBalance, 0);
179+
const totalDeprecatedShare = Object.values(providerShares).reduce((s, p) => s + p.disable, 0);
180+
181+
console.log(`\nTotal BTC in analyzed wallets: ${totalBTC.toFixed(8)} BTC`);
182+
console.log(`Total BTC share of deprecated operators: ${totalDeprecatedShare.toFixed(2)} BTC`);
183+
console.log(`Percentage held by deprecated operators: ${(totalDeprecatedShare / totalBTC * 100).toFixed(2)}%`);
184+
185+
console.log('\n⚠️ IMPORTANT NOTES:');
186+
console.log('1. The "equal-split" is a CALCULATION METHOD, not how threshold signatures work');
187+
console.log('2. In reality, ALL 100 operators must participate for wallet actions (51/100 threshold)');
188+
console.log('3. The deprecated operators do not individually "own" their share');
189+
console.log('4. What matters: which WALLETS contain deprecated operators (all 24 do)');
190+
console.log('5. For sweeps: need active operators to coordinate, not individual BTC shares');
191+
192+
console.log('\n✅ CORRECT INTERPRETATION:');
193+
console.log('- All 24 wallets (5,923.91 BTC) contain deprecated operators');
194+
console.log('- Natural draining or manual sweeps affect the ENTIRE wallet, not per-operator');
195+
console.log('- Coordination needed: active operators from STAKED, P2P, BOAR (and NUCO for 20 wallets)');
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"contractAddress": "0x5e4861a80B55f035D899f66772117F00FA0E8e7B",
3+
"abi": [
4+
{
5+
"inputs": [
6+
{
7+
"internalType": "bytes20",
8+
"name": "walletPubKeyHash",
9+
"type": "bytes20"
10+
}
11+
],
12+
"name": "wallets",
13+
"outputs": [
14+
{
15+
"components": [
16+
{
17+
"internalType": "bytes32",
18+
"name": "ecdsaWalletID",
19+
"type": "bytes32"
20+
},
21+
{
22+
"internalType": "bytes32",
23+
"name": "mainUtxoHash",
24+
"type": "bytes32"
25+
},
26+
{
27+
"internalType": "uint64",
28+
"name": "pendingRedemptionsValue",
29+
"type": "uint64"
30+
},
31+
{
32+
"internalType": "uint32",
33+
"name": "createdAt",
34+
"type": "uint32"
35+
},
36+
{
37+
"internalType": "uint32",
38+
"name": "movingFundsRequestedAt",
39+
"type": "uint32"
40+
},
41+
{
42+
"internalType": "uint32",
43+
"name": "closingStartedAt",
44+
"type": "uint32"
45+
},
46+
{
47+
"internalType": "uint32",
48+
"name": "pendingMovedFundsSweepRequestsCount",
49+
"type": "uint32"
50+
},
51+
{
52+
"internalType": "enum Wallets.WalletState",
53+
"name": "state",
54+
"type": "uint8"
55+
},
56+
{
57+
"internalType": "bytes32",
58+
"name": "movingFundsTargetWalletsCommitmentHash",
59+
"type": "bytes32"
60+
}
61+
],
62+
"internalType": "struct Wallets.Wallet",
63+
"name": "",
64+
"type": "tuple"
65+
}
66+
],
67+
"stateMutability": "view",
68+
"type": "function"
69+
}
70+
]
71+
}

0 commit comments

Comments
 (0)