Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions scripts/wallet-operator-mapper/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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

# 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
16 changes: 16 additions & 0 deletions scripts/wallet-operator-mapper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Environment
.env

# Dependencies
node_modules/
package-lock.json

# Output
wallet-operator-mapping.json

# Data files
data/

# Logs
*.log
npm-debug.log*
123 changes: 123 additions & 0 deletions scripts/wallet-operator-mapper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# 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. 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.

**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
195 changes: 195 additions & 0 deletions scripts/wallet-operator-mapper/analyze-per-operator.js
Original file line number Diff line number Diff line change
@@ -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)');
Loading