suix_getCommitteeInfo
Returns information about the current validator committee for a specific epoch on the Sui network.
Overview​
The suix_getCommitteeInfo
method provides detailed information about the validator committee for a given epoch. This includes the complete list of validators, their voting power, stake distribution, and committee composition. The committee information is essential for understanding network governance, validator participation, and consensus mechanisms on Sui.
Parameters​
Parameter | Type | Required | Description |
---|---|---|---|
epoch | string | No | The epoch to query committee info for (defaults to current epoch) |
Epoch Parameter​
- If not provided, returns committee info for the current epoch
- Must be a valid epoch number as a string
- Historical epoch data may have retention limits
Returns​
Returns comprehensive committee information for the specified epoch.
Field | Type | Description |
---|---|---|
epoch | string | The epoch number for this committee |
validators | array | Array of validator objects with details |
totalStake | string | Total stake delegated to all validators |
quorumThreshold | string | Minimum stake needed for consensus quorum |
Validator Object Structure​
Each validator in the committee contains:
Field | Type | Description |
---|---|---|
authorityPubkeyBytes | string | Validator's authority public key |
networkPubkeyBytes | string | Network public key for consensus |
accountAddress | string | Validator's Sui address |
networkAddress | string | Network endpoint address |
hostname | string | Validator hostname |
port | string | Network port |
stake | string | Validator's voting power/stake |
votingPower | number | Normalized voting power (0-10000) |
Code Examples​
- cURL
- JavaScript
- Python
# Get current epoch committee info
curl -X POST https://sui-mainnet.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "suix_getCommitteeInfo",
"params": [],
"id": 1
}'
# Get committee info for specific epoch
curl -X POST https://sui-mainnet.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "suix_getCommitteeInfo",
"params": ["245"],
"id": 1
}'
import { SuiClient } from '@mysten/sui.js/client';
const client = new SuiClient({
url: 'https://sui-mainnet.dwellir.com/YOUR_API_KEY'
});
// Get current committee information
async function getCurrentCommitteeInfo() {
try {
const committeeInfo = await client.getCommitteeInfo();
console.log(`Committee for Epoch ${committeeInfo.epoch}`);
console.log(`Total Validators: ${committeeInfo.validators.length}`);
console.log(`Total Stake: ${Number(committeeInfo.totalStake) / 1_000_000_000} SUI`);
console.log(`Quorum Threshold: ${Number(committeeInfo.quorumThreshold) / 1_000_000_000} SUI`);
return committeeInfo;
} catch (error) {
console.error('Failed to get committee info:', error);
return null;
}
}
// Get committee info for specific epoch
async function getCommitteeInfoByEpoch(epoch) {
try {
const committeeInfo = await client.getCommitteeInfo({ epoch: epoch.toString() });
console.log(`Historical Committee for Epoch ${epoch}`);
console.log(`Validators: ${committeeInfo.validators.length}`);
return committeeInfo;
} catch (error) {
console.error(`Failed to get committee info for epoch ${epoch}:`, error);
return null;
}
}
// Committee analyzer with detailed insights
class CommitteeAnalyzer {
constructor(client) {
this.client = client;
}
async analyzeCommittee(epoch = null) {
const committeeInfo = epoch ?
await this.client.getCommitteeInfo({ epoch: epoch.toString() }) :
await this.client.getCommitteeInfo();
if (!committeeInfo) {
return { error: 'Failed to retrieve committee information' };
}
const analysis = {
epoch: committeeInfo.epoch,
basicStats: this.calculateBasicStats(committeeInfo),
stakeDistribution: this.analyzeStakeDistribution(committeeInfo.validators),
networkDistribution: this.analyzeNetworkDistribution(committeeInfo.validators),
governanceMetrics: this.calculateGovernanceMetrics(committeeInfo),
validatorDetails: this.processValidatorDetails(committeeInfo.validators)
};
return analysis;
}
calculateBasicStats(committeeInfo) {
const validators = committeeInfo.validators;
const totalStake = BigInt(committeeInfo.totalStake);
const stakes = validators.map(v => BigInt(v.stake));
return {
totalValidators: validators.length,
totalStakeSUI: Number(totalStake) / 1_000_000_000,
quorumThresholdSUI: Number(committeeInfo.quorumThreshold) / 1_000_000_000,
averageStakeSUI: stakes.length > 0 ?
Number(stakes.reduce((sum, stake) => sum + stake, BigInt(0))) / stakes.length / 1_000_000_000 : 0,
medianStakeSUI: this.calculateMedianStake(stakes),
minStakeSUI: stakes.length > 0 ? Number(stakes.reduce((min, stake) => stake < min ? stake : min)) / 1_000_000_000 : 0,
maxStakeSUI: stakes.length > 0 ? Number(stakes.reduce((max, stake) => stake > max ? stake : max)) / 1_000_000_000 : 0
};
}
calculateMedianStake(stakes) {
if (stakes.length === 0) return 0;
const sorted = [...stakes].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
const middle = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return Number(sorted[middle - 1] + sorted[middle]) / 2 / 1_000_000_000;
} else {
return Number(sorted[middle]) / 1_000_000_000;
}
}
analyzeStakeDistribution(validators) {
const stakes = validators.map(v => ({
address: v.accountAddress,
stake: BigInt(v.stake),
votingPower: v.votingPower
})).sort((a, b) => b.stake > a.stake ? 1 : -1);
const totalStake = stakes.reduce((sum, v) => sum + v.stake, BigInt(0));
const distribution = {
top10: stakes.slice(0, 10).map(v => ({
address: v.address,
stakeSUI: Number(v.stake) / 1_000_000_000,
percentage: Number(v.stake * BigInt(10000) / totalStake) / 100,
votingPower: v.votingPower
})),
concentration: {
top5Percentage: 0,
top10Percentage: 0,
top20Percentage: 0,
nakamotoCoefficient: 0
}
};
// Calculate concentration metrics
const top5Stake = stakes.slice(0, 5).reduce((sum, v) => sum + v.stake, BigInt(0));
const top10Stake = stakes.slice(0, 10).reduce((sum, v) => sum + v.stake, BigInt(0));
const top20Stake = stakes.slice(0, Math.min(20, stakes.length)).reduce((sum, v) => sum + v.stake, BigInt(0));
distribution.concentration.top5Percentage = Number(top5Stake * BigInt(10000) / totalStake) / 100;
distribution.concentration.top10Percentage = Number(top10Stake * BigInt(10000) / totalStake) / 100;
distribution.concentration.top20Percentage = Number(top20Stake * BigInt(10000) / totalStake) / 100;
// Calculate Nakamoto coefficient (minimum validators needed for >33% stake)
let cumulativeStake = BigInt(0);
const thresholdStake = totalStake * BigInt(33) / BigInt(100);
for (let i = 0; i < stakes.length; i++) {
cumulativeStake += stakes[i].stake;
if (cumulativeStake > thresholdStake) {
distribution.concentration.nakamotoCoefficient = i + 1;
break;
}
}
return distribution;
}
analyzeNetworkDistribution(validators) {
const networkStats = {
uniqueHosts: new Set(),
portDistribution: {},
networkAddresses: [],
duplicateHosts: []
};
const hostCounts = {};
validators.forEach(validator => {
const hostname = validator.hostname;
const port = validator.port;
networkStats.uniqueHosts.add(hostname);
networkStats.networkAddresses.push({
address: validator.accountAddress,
hostname: hostname,
port: port,
networkAddress: validator.networkAddress
});
// Count port usage
networkStats.portDistribution[port] = (networkStats.portDistribution[port] || 0) + 1;
// Track host duplicates
hostCounts[hostname] = (hostCounts[hostname] || 0) + 1;
});
// Find duplicate hosts
Object.entries(hostCounts).forEach(([hostname, count]) => {
if (count > 1) {
networkStats.duplicateHosts.push({ hostname, count });
}
});
return {
totalUniqueHosts: networkStats.uniqueHosts.size,
totalValidators: validators.length,
hostDiversityScore: networkStats.uniqueHosts.size / validators.length,
portDistribution: networkStats.portDistribution,
duplicateHosts: networkStats.duplicateHosts,
networkAddresses: networkStats.networkAddresses.slice(0, 20) // Limit output
};
}
calculateGovernanceMetrics(committeeInfo) {
const totalStake = BigInt(committeeInfo.totalStake);
const quorum = BigInt(committeeInfo.quorumThreshold);
const validators = committeeInfo.validators;
// Calculate various thresholds
const byzantineThreshold = totalStake * BigInt(33) / BigInt(100); // 1/3
const superMajority = totalStake * BigInt(67) / BigInt(100); // 2/3
const metrics = {
quorumPercentage: Number(quorum * BigInt(10000) / totalStake) / 100,
byzantineToleranceThreshold: Number(byzantineThreshold) / 1_000_000_000,
superMajorityThreshold: Number(superMajority) / 1_000_000_000,
decentralizationMetrics: {
giniCoefficient: this.calculateGiniCoefficient(validators.map(v => BigInt(v.stake))),
herfindahlIndex: this.calculateHerfindahlIndex(validators.map(v => BigInt(v.stake)), totalStake),
validatorsFor51Percent: this.calculateValidatorsForThreshold(validators, 51),
validatorsFor67Percent: this.calculateValidatorsForThreshold(validators, 67)
}
};
return metrics;
}
calculateGiniCoefficient(stakes) {
if (stakes.length === 0) return 0;
const sorted = [...stakes].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
const n = BigInt(sorted.length);
let numerator = BigInt(0);
let denominator = BigInt(0);
for (let i = 0; i < sorted.length; i++) {
numerator += BigInt(i + 1) * sorted[i];
denominator += sorted[i];
}
if (denominator === BigInt(0)) return 0;
const gini = Number((BigInt(2) * numerator) * BigInt(10000) / (n * denominator)) / 10000 -
Number(n + BigInt(1)) / Number(n);
return Math.max(0, Math.min(1, gini));
}
calculateHerfindahlIndex(stakes, totalStake) {
if (totalStake === BigInt(0)) return 0;
let hhi = BigInt(0);
for (const stake of stakes) {
const share = stake * BigInt(10000) / totalStake; // Share as basis points
hhi += share * share;
}
return Number(hhi) / 100000000; // Convert back to decimal
}
calculateValidatorsForThreshold(validators, thresholdPercent) {
const totalStake = validators.reduce((sum, v) => sum + BigInt(v.stake), BigInt(0));
const threshold = totalStake * BigInt(thresholdPercent) / BigInt(100);
const sorted = [...validators].sort((a, b) => BigInt(b.stake) > BigInt(a.stake) ? 1 : -1);
let cumulativeStake = BigInt(0);
for (let i = 0; i < sorted.length; i++) {
cumulativeStake += BigInt(sorted[i].stake);
if (cumulativeStake >= threshold) {
return i + 1;
}
}
return sorted.length;
}
processValidatorDetails(validators) {
return validators.slice(0, 50).map(validator => ({ // Limit to first 50 for performance
accountAddress: validator.accountAddress,
stakeSUI: Number(validator.stake) / 1_000_000_000,
votingPower: validator.votingPower,
hostname: validator.hostname,
port: validator.port,
networkAddress: validator.networkAddress
}));
}
}
// Committee comparison tool
class CommitteeComparator {
constructor(client) {
this.client = client;
}
async compareEpochs(epoch1, epoch2) {
try {
const [committee1, committee2] = await Promise.all([
this.client.getCommitteeInfo({ epoch: epoch1.toString() }),
this.client.getCommitteeInfo({ epoch: epoch2.toString() })
]);
const comparison = {
epochs: { epoch1, epoch2 },
changes: this.calculateChanges(committee1, committee2),
validatorChanges: this.compareValidators(committee1.validators, committee2.validators),
stakeChanges: this.compareStakes(committee1, committee2),
networkChanges: this.compareNetworks(committee1.validators, committee2.validators)
};
return comparison;
} catch (error) {
console.error('Error comparing committees:', error);
return { error: error.message };
}
}
calculateChanges(committee1, committee2) {
const totalStake1 = BigInt(committee1.totalStake);
const totalStake2 = BigInt(committee2.totalStake);
const stakeChange = totalStake2 - totalStake1;
return {
validatorCountChange: committee2.validators.length - committee1.validators.length,
totalStakeChange: Number(stakeChange) / 1_000_000_000,
totalStakeChangePercent: totalStake1 > 0 ?
Number(stakeChange * BigInt(10000) / totalStake1) / 100 : 0
};
}
compareValidators(validators1, validators2) {
const addresses1 = new Set(validators1.map(v => v.accountAddress));
const addresses2 = new Set(validators2.map(v => v.accountAddress));
const added = validators2.filter(v => !addresses1.has(v.accountAddress));
const removed = validators1.filter(v => !addresses2.has(v.accountAddress));
const continued = validators2.filter(v => addresses1.has(v.accountAddress));
return {
added: added.map(v => ({
address: v.accountAddress,
stake: Number(v.stake) / 1_000_000_000,
votingPower: v.votingPower
})),
removed: removed.map(v => ({
address: v.accountAddress,
stake: Number(v.stake) / 1_000_000_000,
votingPower: v.votingPower
})),
continuedCount: continued.length,
addedCount: added.length,
removedCount: removed.length
};
}
compareStakes(committee1, committee2) {
const validatorMap1 = new Map(committee1.validators.map(v => [v.accountAddress, BigInt(v.stake)]));
const validatorMap2 = new Map(committee2.validators.map(v => [v.accountAddress, BigInt(v.stake)]));
const stakeChanges = [];
for (const [address, stake2] of validatorMap2) {
const stake1 = validatorMap1.get(address);
if (stake1) {
const change = stake2 - stake1;
if (change !== BigInt(0)) {
stakeChanges.push({
address,
previousStake: Number(stake1) / 1_000_000_000,
newStake: Number(stake2) / 1_000_000_000,
change: Number(change) / 1_000_000_000,
changePercent: Number(change * BigInt(10000) / stake1) / 100
});
}
}
}
return stakeChanges.sort((a, b) => Math.abs(b.change) - Math.abs(a.change));
}
compareNetworks(validators1, validators2) {
const network1 = new Set(validators1.map(v => `${v.hostname}:${v.port}`));
const network2 = new Set(validators2.map(v => `${v.hostname}:${v.port}`));
const newEndpoints = [...network2].filter(endpoint => !network1.has(endpoint));
const removedEndpoints = [...network1].filter(endpoint => !network2.has(endpoint));
return {
newEndpoints,
removedEndpoints,
endpointChanges: newEndpoints.length + removedEndpoints.length
};
}
}
// Committee monitoring system
class CommitteeMonitor {
constructor(client) {
this.client = client;
this.monitoringData = new Map();
this.alerts = [];
}
async startMonitoring(checkIntervalMinutes = 60) {
console.log(`📊 Starting committee monitoring (every ${checkIntervalMinutes} minutes)`);
const monitor = async () => {
try {
const currentCommittee = await this.client.getCommitteeInfo();
const epochId = currentCommittee.epoch;
// Check if this is a new epoch
const previousData = this.monitoringData.get('latest');
if (previousData && previousData.epoch !== epochId) {
console.log(`🔄 New epoch detected: ${previousData.epoch} → ${epochId}`);
// Analyze epoch transition
const transition = await this.analyzeEpochTransition(previousData, currentCommittee);
this.checkForAlerts(transition);
}
// Store current data
this.monitoringData.set('latest', currentCommittee);
this.monitoringData.set(`epoch_${epochId}`, currentCommittee);
console.log(`Committee Status - Epoch ${epochId}: ${currentCommittee.validators.length} validators, ${Number(currentCommittee.totalStake) / 1_000_000_000:.0f} SUI total stake`);
} catch (error) {
console.error('Error in committee monitoring:', error);
}
};
// Initial check
await monitor();
// Set up periodic monitoring
const intervalId = setInterval(monitor, checkIntervalMinutes * 60 * 1000);
return () => {
clearInterval(intervalId);
console.log('Committee monitoring stopped');
};
}
async analyzeEpochTransition(previousCommittee, currentCommittee) {
const comparator = new CommitteeComparator(this.client);
return await comparator.compareEpochs(previousCommittee.epoch, currentCommittee.epoch);
}
checkForAlerts(transition) {
const alerts = [];
// Check for significant validator changes
if (transition.validatorChanges.addedCount > 2) {
alerts.push({
type: 'SIGNIFICANT_VALIDATOR_ADDITIONS',
message: `${transition.validatorChanges.addedCount} new validators joined the committee`,
severity: 'INFO'
});
}
if (transition.validatorChanges.removedCount > 2) {
alerts.push({
type: 'SIGNIFICANT_VALIDATOR_REMOVALS',
message: `${transition.validatorChanges.removedCount} validators left the committee`,
severity: 'WARNING'
});
}
// Check for large stake changes
if (Math.abs(transition.changes.totalStakeChangePercent) > 10) {
alerts.push({
type: 'LARGE_STAKE_CHANGE',
message: `Total stake changed by ${transition.changes.totalStakeChangePercent.toFixed(1)}%`,
severity: 'WARNING'
});
}
// Store and log alerts
alerts.forEach(alert => {
this.alerts.push({ ...alert, timestamp: Date.now() });
console.warn(`🚨 [${alert.severity}] ${alert.message}`);
});
return alerts;
}
}
// Usage examples
// Get current committee info
const currentCommittee = await getCurrentCommitteeInfo();
if (currentCommittee) {
console.log('\nTop 5 validators by stake:');
currentCommittee.validators
.sort((a, b) => BigInt(b.stake) > BigInt(a.stake) ? 1 : -1)
.slice(0, 5)
.forEach((validator, index) => {
console.log(`${index + 1}. ${validator.accountAddress.substring(0, 16)}...`);
console.log(` Stake: ${Number(validator.stake) / 1_000_000_000} SUI`);
console.log(` Voting Power: ${validator.votingPower}`);
console.log(` Host: ${validator.hostname}:${validator.port}`);
});
}
// Analyze committee composition
const analyzer = new CommitteeAnalyzer(client);
const analysis = await analyzer.analyzeCommittee();
if (analysis && !analysis.error) {
console.log('\nCommittee Analysis:');
console.log(`Epoch: ${analysis.epoch}`);
console.log(`Total Validators: ${analysis.basicStats.totalValidators}`);
console.log(`Total Stake: ${analysis.basicStats.totalStakeSUI.toFixed(0)} SUI`);
console.log(`Average Stake: ${analysis.basicStats.averageStakeSUI.toFixed(2)} SUI`);
console.log(`Top 10 Control: ${analysis.stakeDistribution.concentration.top10Percentage.toFixed(1)}%`);
console.log(`Nakamoto Coefficient: ${analysis.stakeDistribution.concentration.nakamotoCoefficient}`);
console.log(`Host Diversity Score: ${analysis.networkDistribution.hostDiversityScore.toFixed(3)}`);
console.log(`Gini Coefficient: ${analysis.governanceMetrics.decentralizationMetrics.giniCoefficient.toFixed(3)}`);
}
// Compare different epochs
const comparator = new CommitteeComparator(client);
const comparison = await comparator.compareEpochs('245', '246');
if (comparison && !comparison.error) {
console.log('\nEpoch Comparison (245 → 246):');
console.log(`Validator Count Change: ${comparison.changes.validatorCountChange}`);
console.log(`Total Stake Change: ${comparison.changes.totalStakeChange.toFixed(2)} SUI`);
console.log(`Validators Added: ${comparison.validatorChanges.addedCount}`);
console.log(`Validators Removed: ${comparison.validatorChanges.removedCount}`);
}
// Start monitoring (uncomment to run)
// const monitor = new CommitteeMonitor(client);
// const stopMonitoring = await monitor.startMonitoring(60); // Check every hour
// Stop monitoring after demo period
// setTimeout(stopMonitoring, 300000); // Stop after 5 minutes
import requests
import json
import time
import statistics
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass
import threading
@dataclass
class ValidatorInfo:
account_address: str
authority_pubkey_bytes: str
network_pubkey_bytes: str
network_address: str
hostname: str
port: str
stake: int
voting_power: int
class SuiCommitteeClient:
def __init__(self, rpc_url: str):
self.rpc_url = rpc_url
def get_committee_info(self, epoch: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""Get committee information for specified epoch or current epoch"""
params = [epoch] if epoch else []
payload = {
"jsonrpc": "2.0",
"method": "suix_getCommitteeInfo",
"params": params,
"id": 1
}
try:
response = requests.post(
self.rpc_url,
headers={'Content-Type': 'application/json'},
data=json.dumps(payload),
timeout=30
)
result = response.json()
if 'error' in result:
print(f"RPC Error: {result['error']}")
return None
return result['result']
except Exception as e:
print(f"Error fetching committee info: {e}")
return None
class CommitteeAnalyzer:
def __init__(self, client: SuiCommitteeClient):
self.client = client
def analyze_committee(self, epoch: Optional[str] = None) -> Dict[str, Any]:
"""Analyze committee composition and provide detailed insights"""
committee_info = self.client.get_committee_info(epoch)
if not committee_info:
return {'error': 'Failed to retrieve committee information'}
validators = committee_info['validators']
total_stake = int(committee_info['totalStake'])
analysis = {
'epoch': committee_info['epoch'],
'basic_stats': self._calculate_basic_stats(committee_info),
'stake_distribution': self._analyze_stake_distribution(validators, total_stake),
'network_distribution': self._analyze_network_distribution(validators),
'governance_metrics': self._calculate_governance_metrics(committee_info),
'validator_details': self._process_validator_details(validators)
}
return analysis
def _calculate_basic_stats(self, committee_info: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate basic statistics about the committee"""
validators = committee_info['validators']
total_stake = int(committee_info['totalStake'])
stakes = [int(v['stake']) for v in validators]
return {
'total_validators': len(validators),
'total_stake_sui': total_stake / 1_000_000_000,
'quorum_threshold_sui': int(committee_info['quorumThreshold']) / 1_000_000_000,
'average_stake_sui': statistics.mean(stakes) / 1_000_000_000 if stakes else 0,
'median_stake_sui': statistics.median(stakes) / 1_000_000_000 if stakes else 0,
'min_stake_sui': min(stakes) / 1_000_000_000 if stakes else 0,
'max_stake_sui': max(stakes) / 1_000_000_000 if stakes else 0,
'stake_std_dev': statistics.stdev(stakes) / 1_000_000_000 if len(stakes) > 1 else 0
}
def _analyze_stake_distribution(self, validators: List[Dict], total_stake: int) -> Dict[str, Any]:
"""Analyze stake distribution and concentration"""
# Sort validators by stake descending
sorted_validators = sorted(validators, key=lambda v: int(v['stake']), reverse=True)
distribution = {
'top_10': [
{
'address': v['accountAddress'],
'stake_sui': int(v['stake']) / 1_000_000_000,
'percentage': int(v['stake']) / total_stake * 100,
'voting_power': v['votingPower']
}
for v in sorted_validators[:10]
],
'concentration': self._calculate_concentration_metrics(sorted_validators, total_stake)
}
return distribution
def _calculate_concentration_metrics(self, sorted_validators: List[Dict], total_stake: int) -> Dict[str, Any]:
"""Calculate various concentration metrics"""
stakes = [int(v['stake']) for v in sorted_validators]
# Top N percentages
top_5_stake = sum(stakes[:5])
top_10_stake = sum(stakes[:10])
top_20_stake = sum(stakes[:min(20, len(stakes))])
metrics = {
'top_5_percentage': top_5_stake / total_stake * 100,
'top_10_percentage': top_10_stake / total_stake * 100,
'top_20_percentage': top_20_stake / total_stake * 100,
'nakamoto_coefficient': self._calculate_nakamoto_coefficient(stakes, total_stake),
'gini_coefficient': self._calculate_gini_coefficient(stakes),
'herfindahl_index': self._calculate_herfindahl_index(stakes, total_stake)
}
return metrics
def _calculate_nakamoto_coefficient(self, stakes: List[int], total_stake: int) -> int:
"""Calculate Nakamoto coefficient (validators needed for >33% stake)"""
threshold = total_stake * 33 // 100
cumulative = 0
for i, stake in enumerate(stakes):
cumulative += stake
if cumulative > threshold:
return i + 1
return len(stakes)
def _calculate_gini_coefficient(self, stakes: List[int]) -> float:
"""Calculate Gini coefficient for stake distribution"""
if not stakes:
return 0.0
sorted_stakes = sorted(stakes)
n = len(sorted_stakes)
if n == 0 or sum(sorted_stakes) == 0:
return 0.0
numerator = sum((i + 1) * stake for i, stake in enumerate(sorted_stakes))
denominator = n * sum(sorted_stakes)
return (2 * numerator / denominator) - (n + 1) / n
def _calculate_herfindahl_index(self, stakes: List[int], total_stake: int) -> float:
"""Calculate Herfindahl-Hirschman Index"""
if total_stake == 0:
return 0.0
return sum((stake / total_stake) ** 2 for stake in stakes)
def _analyze_network_distribution(self, validators: List[Dict]) -> Dict[str, Any]:
"""Analyze network distribution and diversity"""
hostnames = [v['hostname'] for v in validators]
ports = [v['port'] for v in validators]
hostname_counts = {}
port_counts = {}
for hostname in hostnames:
hostname_counts[hostname] = hostname_counts.get(hostname, 0) + 1
for port in ports:
port_counts[port] = port_counts.get(port, 0) + 1
return {
'total_unique_hosts': len(set(hostnames)),
'total_validators': len(validators),
'host_diversity_score': len(set(hostnames)) / len(validators),
'port_distribution': dict(sorted(port_counts.items())),
'duplicate_hosts': [
{'hostname': hostname, 'count': count}
for hostname, count in hostname_counts.items()
if count > 1
],
'most_common_ports': sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:5]
}
def _calculate_governance_metrics(self, committee_info: Dict[str, Any]) -> Dict[str, Any]:
"""Calculate governance and decentralization metrics"""
total_stake = int(committee_info['totalStake'])
quorum = int(committee_info['quorumThreshold'])
validators = committee_info['validators']
stakes = [int(v['stake']) for v in validators]
return {
'quorum_percentage': quorum / total_stake * 100,
'byzantine_tolerance_threshold': total_stake * 33 // 100 / 1_000_000_000,
'super_majority_threshold': total_stake * 67 // 100 / 1_000_000_000,
'validators_for_51_percent': self._validators_for_threshold(stakes, 51),
'validators_for_67_percent': self._validators_for_threshold(stakes, 67),
'effective_validators': len([s for s in stakes if s > 0])
}
def _validators_for_threshold(self, stakes: List[int], threshold_percent: int) -> int:
"""Calculate minimum validators needed for given percentage of stake"""
total_stake = sum(stakes)
threshold = total_stake * threshold_percent // 100
sorted_stakes = sorted(stakes, reverse=True)
cumulative = 0
for i, stake in enumerate(sorted_stakes):
cumulative += stake
if cumulative >= threshold:
return i + 1
return len(stakes)
def _process_validator_details(self, validators: List[Dict]) -> List[Dict[str, Any]]:
"""Process validator details for output (limit for performance)"""
return [
{
'account_address': v['accountAddress'],
'stake_sui': int(v['stake']) / 1_000_000_000,
'voting_power': v['votingPower'],
'hostname': v['hostname'],
'port': v['port'],
'network_address': v['networkAddress']
}
for v in validators[:50] # Limit to first 50
]
class CommitteeComparator:
def __init__(self, client: SuiCommitteeClient):
self.client = client
def compare_epochs(self, epoch1: str, epoch2: str) -> Dict[str, Any]:
"""Compare committee composition between two epochs"""
committee1 = self.client.get_committee_info(epoch1)
committee2 = self.client.get_committee_info(epoch2)
if not committee1 or not committee2:
return {'error': 'Failed to retrieve committee information for comparison'}
comparison = {
'epochs': {'epoch1': epoch1, 'epoch2': epoch2},
'basic_changes': self._calculate_basic_changes(committee1, committee2),
'validator_changes': self._compare_validators(committee1['validators'], committee2['validators']),
'stake_changes': self._compare_stakes(committee1['validators'], committee2['validators']),
'network_changes': self._compare_networks(committee1['validators'], committee2['validators'])
}
return comparison
def _calculate_basic_changes(self, committee1: Dict, committee2: Dict) -> Dict[str, Any]:
"""Calculate basic changes between committees"""
total_stake1 = int(committee1['totalStake'])
total_stake2 = int(committee2['totalStake'])
stake_change = total_stake2 - total_stake1
return {
'validator_count_change': len(committee2['validators']) - len(committee1['validators']),
'total_stake_change_sui': stake_change / 1_000_000_000,
'total_stake_change_percent': (stake_change / total_stake1 * 100) if total_stake1 > 0 else 0
}
def _compare_validators(self, validators1: List[Dict], validators2: List[Dict]) -> Dict[str, Any]:
"""Compare validator sets between epochs"""
addresses1 = set(v['accountAddress'] for v in validators1)
addresses2 = set(v['accountAddress'] for v in validators2)
added = [v for v in validators2 if v['accountAddress'] not in addresses1]
removed = [v for v in validators1 if v['accountAddress'] not in addresses2]
continued = [v for v in validators2 if v['accountAddress'] in addresses1]
return {
'added': [
{
'address': v['accountAddress'],
'stake_sui': int(v['stake']) / 1_000_000_000,
'voting_power': v['votingPower']
}
for v in added
],
'removed': [
{
'address': v['accountAddress'],
'stake_sui': int(v['stake']) / 1_000_000_000,
'voting_power': v['votingPower']
}
for v in removed
],
'added_count': len(added),
'removed_count': len(removed),
'continued_count': len(continued)
}
def _compare_stakes(self, validators1: List[Dict], validators2: List[Dict]) -> List[Dict[str, Any]]:
"""Compare stake changes for validators present in both epochs"""
stake_map1 = {v['accountAddress']: int(v['stake']) for v in validators1}
stake_map2 = {v['accountAddress']: int(v['stake']) for v in validators2}
stake_changes = []
for address, stake2 in stake_map2.items():
if address in stake_map1:
stake1 = stake_map1[address]
change = stake2 - stake1
if change != 0:
stake_changes.append({
'address': address,
'previous_stake_sui': stake1 / 1_000_000_000,
'new_stake_sui': stake2 / 1_000_000_000,
'change_sui': change / 1_000_000_000,
'change_percent': (change / stake1 * 100) if stake1 > 0 else 0
})
return sorted(stake_changes, key=lambda x: abs(x['change_sui']), reverse=True)
def _compare_networks(self, validators1: List[Dict], validators2: List[Dict]) -> Dict[str, Any]:
"""Compare network configurations between epochs"""
endpoints1 = set(f"{v['hostname']}:{v['port']}" for v in validators1)
endpoints2 = set(f"{v['hostname']}:{v['port']}" for v in validators2)
return {
'new_endpoints': list(endpoints2 - endpoints1),
'removed_endpoints': list(endpoints1 - endpoints2),
'endpoint_changes': len(endpoints1.symmetric_difference(endpoints2))
}
class CommitteeMonitor:
def __init__(self, client: SuiCommitteeClient):
self.client = client
self.monitoring_data = {}
self.alerts = []
self.is_monitoring = False
self.monitor_thread = None
def start_monitoring(self, check_interval_minutes: int = 60, duration_hours: int = 24):
"""Start monitoring committee changes"""
if self.is_monitoring:
print("Committee monitoring already in progress")
return
print(f"📊 Starting committee monitoring (every {check_interval_minutes} minutes)")
self.is_monitoring = True
def monitor_loop():
start_time = time.time()
end_time = start_time + (duration_hours * 3600)
while self.is_monitoring and time.time() < end_time:
try:
current_committee = self.client.get_committee_info()
if current_committee:
epoch_id = current_committee['epoch']
# Check for epoch change
previous_data = self.monitoring_data.get('latest')
if previous_data and previous_data['epoch'] != epoch_id:
print(f"🔄 New epoch detected: {previous_data['epoch']} → {epoch_id}")
self._analyze_epoch_transition(previous_data, current_committee)
# Store current data
self.monitoring_data['latest'] = current_committee
self.monitoring_data[f'epoch_{epoch_id}'] = current_committee
validator_count = len(current_committee['validators'])
total_stake_sui = int(current_committee['totalStake']) / 1_000_000_000
print(f"Committee Status - Epoch {epoch_id}: {validator_count} validators, {total_stake_sui:.0f} SUI total stake")
time.sleep(check_interval_minutes * 60)
except Exception as e:
print(f"Error in committee monitoring: {e}")
time.sleep(check_interval_minutes * 60)
self.is_monitoring = False
print("Committee monitoring completed")
self.monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
self.monitor_thread.start()
def stop_monitoring(self):
"""Stop committee monitoring"""
self.is_monitoring = False
if self.monitor_thread:
self.monitor_thread.join(timeout=5)
print("Committee monitoring stopped")
def _analyze_epoch_transition(self, previous_committee: Dict, current_committee: Dict):
"""Analyze changes between epochs and generate alerts"""
comparator = CommitteeComparator(self.client)
comparison = comparator._calculate_basic_changes(previous_committee, current_committee)
alerts = []
# Check for significant changes
if abs(comparison['validator_count_change']) > 2:
alerts.append({
'type': 'SIGNIFICANT_VALIDATOR_CHANGE',
'message': f"Validator count changed by {comparison['validator_count_change']}",
'severity': 'INFO' if comparison['validator_count_change'] > 0 else 'WARNING'
})
if abs(comparison['total_stake_change_percent']) > 10:
alerts.append({
'type': 'LARGE_STAKE_CHANGE',
'message': f"Total stake changed by {comparison['total_stake_change_percent']:.1f}%",
'severity': 'WARNING'
})
# Store and log alerts
for alert in alerts:
alert['timestamp'] = time.time()
self.alerts.append(alert)
print(f"🚨 [{alert['severity']}] {alert['message']}")
# Usage examples
client = SuiCommitteeClient('https://sui-mainnet.dwellir.com/YOUR_API_KEY')
# Example 1: Get current committee information
print("Getting current committee information...")
current_committee = client.get_committee_info()
if current_committee:
print(f"Current Committee - Epoch {current_committee['epoch']}")
print(f" Total Validators: {len(current_committee['validators'])}")
print(f" Total Stake: {int(current_committee['totalStake']) / 1_000_000_000:.0f} SUI")
print(f" Quorum Threshold: {int(current_committee['quorumThreshold']) / 1_000_000_000:.0f} SUI")
# Show top validators by stake
validators = current_committee['validators']
sorted_validators = sorted(validators, key=lambda v: int(v['stake']), reverse=True)
print("\nTop 5 Validators by Stake:")
for i, validator in enumerate(sorted_validators[:5], 1):
stake_sui = int(validator['stake']) / 1_000_000_000
print(f" {i}. {validator['accountAddress'][:16]}...")
print(f" Stake: {stake_sui:.0f} SUI")
print(f" Voting Power: {validator['votingPower']}")
print(f" Host: {validator['hostname']}:{validator['port']}")
# Example 2: Analyze committee composition
print("\nAnalyzing committee composition...")
analyzer = CommitteeAnalyzer(client)
analysis = analyzer.analyze_committee()
if 'error' not in analysis:
stats = analysis['basic_stats']
concentration = analysis['stake_distribution']['concentration']
network = analysis['network_distribution']
governance = analysis['governance_metrics']
print(f"Committee Analysis for Epoch {analysis['epoch']}:")
print(f" Total Validators: {stats['total_validators']}")
print(f" Total Stake: {stats['total_stake_sui']:.0f} SUI")
print(f" Average Stake: {stats['average_stake_sui']:.2f} SUI")
print(f" Median Stake: {stats['median_stake_sui']:.2f} SUI")
print(f"\nStake Concentration:")
print(f" Top 5 Control: {concentration['top_5_percentage']:.1f}%")
print(f" Top 10 Control: {concentration['top_10_percentage']:.1f}%")
print(f" Nakamoto Coefficient: {concentration['nakamoto_coefficient']}")
print(f" Gini Coefficient: {concentration['gini_coefficient']:.3f}")
print(f" Herfindahl Index: {concentration['herfindahl_index']:.4f}")
print(f"\nNetwork Distribution:")
print(f" Unique Hosts: {network['total_unique_hosts']}")
print(f" Host Diversity Score: {network['host_diversity_score']:.3f}")
if network['duplicate_hosts']:
print(f" Duplicate Hosts: {len(network['duplicate_hosts'])}")
for dup in network['duplicate_hosts'][:3]:
print(f" {dup['hostname']}: {dup['count']} validators")
print(f"\nGovernance Metrics:")
print(f" Validators for 51%: {governance['validators_for_51_percent']}")
print(f" Validators for 67%: {governance['validators_for_67_percent']}")
print(f" Byzantine Tolerance: {governance['byzantine_tolerance_threshold']:.0f} SUI")
# Example 3: Compare different epochs
print("\nComparing committee across epochs...")
comparator = CommitteeComparator(client)
# Get current epoch for comparison
current_epoch = analysis['epoch'] if 'epoch' in analysis else '250'
previous_epoch = str(int(current_epoch) - 1)
comparison = comparator.compare_epochs(previous_epoch, current_epoch)
if 'error' not in comparison:
changes = comparison['basic_changes']
validator_changes = comparison['validator_changes']
print(f"Epoch Comparison ({previous_epoch} → {current_epoch}):")
print(f" Validator Count Change: {changes['validator_count_change']}")
print(f" Total Stake Change: {changes['total_stake_change_sui']:.2f} SUI ({changes['total_stake_change_percent']:.1f}%)")
print(f" Validators Added: {validator_changes['added_count']}")
print(f" Validators Removed: {validator_changes['removed_count']}")
print(f" Validators Continued: {validator_changes['continued_count']}")
if validator_changes['added']:
print(f"\nNew Validators:")
for validator in validator_changes['added'][:3]:
print(f" {validator['address'][:16]}... ({validator['stake_sui']:.0f} SUI)")
if comparison['stake_changes']:
print(f"\nTop Stake Changes:")
for change in comparison['stake_changes'][:3]:
print(f" {change['address'][:16]}...: {change['change_sui']:+.0f} SUI ({change['change_percent']:+.1f}%)")
# Example 4: Start monitoring (uncomment to run continuously)
print("\nSetting up committee monitoring...")
monitor = CommitteeMonitor(client)
# Demonstrate monitoring setup (don't actually run for extended period in example)
print("Monitor configured - would check every hour for committee changes")
print("Use monitor.start_monitoring(60, 24) to monitor for 24 hours")
# Example of what monitoring output looks like
print("\nExample monitoring output:")
print("📊 Starting committee monitoring (every 60 minutes)")
print("Committee Status - Epoch 251: 120 validators, 2,500,000 SUI total stake")
print("🔄 New epoch detected: 251 → 252")
print("🚨 [INFO] Validator count changed by +2")
print("Committee Status - Epoch 252: 122 validators, 2,520,000 SUI total stake")