Skip to content

Commit

Permalink
Quorum operator stake report cli (#1162)
Browse files Browse the repository at this point in the history
  • Loading branch information
pschork authored Jan 27, 2025
1 parent bea2172 commit 3bc722b
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 0 deletions.
12 changes: 12 additions & 0 deletions tools/quorumscan/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
build: clean
go mod tidy
go build -o ./bin/quorumscan ./cmd

clean:
rm -rf ./bin

lint:
golangci-lint run ./...

run: build
./bin/quorumscan --help
181 changes: 181 additions & 0 deletions tools/quorumscan/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package main

import (
"context"
"fmt"
"log"
"math/big"
"os"
"sort"
"strconv"
"strings"

"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/common/geth"
"github.com/Layr-Labs/eigenda/core"
"github.com/Layr-Labs/eigenda/core/eth"
"github.com/Layr-Labs/eigenda/core/thegraph"
"github.com/Layr-Labs/eigenda/tools/quorumscan"
"github.com/Layr-Labs/eigenda/tools/quorumscan/flags"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/urfave/cli"
)

var (
version = ""
gitCommit = ""
gitDate = ""
)

func main() {
app := cli.NewApp()
app.Version = fmt.Sprintf("%s,%s,%s", version, gitCommit, gitDate)
app.Name = "quorumscan"
app.Description = "operator quorum scan"
app.Usage = ""
app.Flags = flags.Flags
app.Action = RunScan
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

func RunScan(ctx *cli.Context) error {
config, err := quorumscan.NewConfig(ctx)
if err != nil {
return err
}

logger, err := common.NewLogger(config.LoggerConfig)
if err != nil {
return err
}

gethClient, err := geth.NewClient(config.EthClientConfig, gethcommon.Address{}, 0, logger)
if err != nil {
logger.Error("Cannot create chain.Client", "err", err)
return err
}

tx, err := eth.NewReader(logger, gethClient, config.BLSOperatorStateRetrieverAddr, config.EigenDAServiceManagerAddr)
if err != nil {
log.Fatalln("could not start eth.NewReader", err)
}
chainState := eth.NewChainState(tx, gethClient)

logger.Info("Connecting to subgraph", "url", config.ChainStateConfig.Endpoint)
ics := thegraph.MakeIndexedChainState(config.ChainStateConfig, chainState, logger)

var blockNumber uint
if config.BlockNumber != 0 {
blockNumber = uint(config.BlockNumber)
} else {
blockNumber, err = ics.GetCurrentBlockNumber()
if err != nil {
return fmt.Errorf("failed to fetch current block number - %s", err)
}
}
logger.Info("Using block number", "block", blockNumber)

operatorState, err := chainState.GetOperatorState(context.Background(), blockNumber, config.QuorumIDs)
if err != nil {
return fmt.Errorf("failed to fetch operator state - %s", err)
}
operators, err := ics.GetIndexedOperators(context.Background(), blockNumber)
if err != nil {
return fmt.Errorf("failed to fetch indexed operators info - %s", err)
}

logger.Info("Queried operator state", "count", len(operators))

operatorIDs := make([]core.OperatorID, 0, len(operators))
for opID := range operators {
operatorIDs = append(operatorIDs, opID)
}
operatorAddresses, err := tx.BatchOperatorIDToAddress(context.Background(), operatorIDs)
if err != nil {
return err
}
operatorIdToAddress := make(map[string]string)
for i := range operatorAddresses {
operatorIdToAddress[operatorIDs[i].Hex()] = strings.ToLower(operatorAddresses[i].Hex())
}

quorumMetrics := quorumscan.QuorumScan(operators, operatorState, logger)
displayResults(quorumMetrics, operatorIdToAddress, config.TopN)
return nil
}

func humanizeEth(value *big.Float) string {
v, _ := value.Float64()
switch {
case v >= 1_000_000:
return fmt.Sprintf("%.2fM", v/1_000_000)
case v >= 1_000:
return fmt.Sprintf("%.2fK", v/1_000)
default:
return fmt.Sprintf("%.2f", v)
}
}

func displayResults(results map[uint8]*quorumscan.QuorumMetrics, operatorIdToAddress map[string]string, topN uint) {
weiToEth := new(big.Float).SetFloat64(1e18)

// Create sorted list of quorums
quorums := make([]uint8, 0, len(results))
for quorum := range results {
quorums = append(quorums, quorum)
}
sort.Slice(quorums, func(i, j int) bool {
return quorums[i] < quorums[j]
})

for _, quorum := range quorums {
tw := table.NewWriter()
rowAutoMerge := table.RowConfig{AutoMerge: true}
operatorHeader := "OPERATOR"
if topN > 0 {
operatorHeader = "TOP " + strconv.Itoa(int(topN)) + " OPERATORS"
}
tw.AppendHeader(table.Row{"QUORUM", operatorHeader, "ADDRESS", "STAKE", "STAKE"}, rowAutoMerge)

total_operators := 0
total_stake_pct := 0.0
total_stake := new(big.Float)
metrics := results[quorum]

// Create sorted list of operators by stake
type operatorInfo struct {
id string
stake float64
pct float64
}
operators := make([]operatorInfo, 0, len(metrics.OperatorStake))
for op, stake := range metrics.OperatorStake {
operators = append(operators, operatorInfo{op, stake, metrics.OperatorStakePct[op]})
}
sort.Slice(operators, func(i, j int) bool {
return operators[i].stake > operators[j].stake
})

for _, op := range operators {
if topN > 0 && uint(total_operators) >= topN {
break
}
stakeInEth := new(big.Float).Quo(new(big.Float).SetFloat64(op.stake), weiToEth)
stakeInEth.SetPrec(64)
total_operators += 1
total_stake.Add(total_stake, stakeInEth)
total_stake_pct += op.pct

tw.AppendRow(table.Row{quorum, op.id, operatorIdToAddress[op.id], humanizeEth(stakeInEth), fmt.Sprintf("%.2f%%", op.pct)})
}
total_stake.SetPrec(64)
tw.AppendFooter(table.Row{"TOTAL", total_operators, total_operators, humanizeEth(total_stake), fmt.Sprintf("%.2f%%", total_stake_pct)})
tw.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
})
fmt.Println(tw.Render())
}
}
65 changes: 65 additions & 0 deletions tools/quorumscan/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package quorumscan

import (
"strconv"
"strings"
"time"

"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/common/geth"
"github.com/Layr-Labs/eigenda/core"
"github.com/Layr-Labs/eigenda/core/thegraph"
"github.com/Layr-Labs/eigenda/tools/quorumscan/flags"
"github.com/urfave/cli"
)

type Config struct {
LoggerConfig common.LoggerConfig
BlockNumber uint64
Workers int
Timeout time.Duration
UseRetrievalClient bool
QuorumIDs []core.QuorumID
TopN uint

ChainStateConfig thegraph.Config
EthClientConfig geth.EthClientConfig

BLSOperatorStateRetrieverAddr string
EigenDAServiceManagerAddr string
}

func ReadConfig(ctx *cli.Context) *Config {
quorumIDsStr := ctx.String(flags.QuorumIDsFlag.Name)
quorumIDs := []core.QuorumID{}

// Parse comma-separated quorum IDs
if quorumIDsStr != "" {
for _, idStr := range strings.Split(quorumIDsStr, ",") {
if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil {
quorumIDs = append(quorumIDs, core.QuorumID(id))
}
}
}

return &Config{
ChainStateConfig: thegraph.ReadCLIConfig(ctx),
EthClientConfig: geth.ReadEthClientConfig(ctx),
BLSOperatorStateRetrieverAddr: ctx.GlobalString(flags.BlsOperatorStateRetrieverFlag.Name),
EigenDAServiceManagerAddr: ctx.GlobalString(flags.EigenDAServiceManagerFlag.Name),
QuorumIDs: quorumIDs,
BlockNumber: ctx.Uint64(flags.BlockNumberFlag.Name),
TopN: ctx.Uint(flags.TopNFlag.Name),
}
}

func NewConfig(ctx *cli.Context) (*Config, error) {
loggerConfig, err := common.ReadLoggerCLIConfig(ctx, flags.FlagPrefix)
if err != nil {
return nil, err
}

config := ReadConfig(ctx)
config.LoggerConfig = *loggerConfig
return config, nil
}
72 changes: 72 additions & 0 deletions tools/quorumscan/flags/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package flags

import (
"github.com/Layr-Labs/eigenda/common"
"github.com/Layr-Labs/eigenda/common/geth"
"github.com/Layr-Labs/eigenda/core/thegraph"
"github.com/urfave/cli"
)

const (
FlagPrefix = ""
envPrefix = "QUORUMSCAN"
)

var (
/* Required Flags*/
BlsOperatorStateRetrieverFlag = cli.StringFlag{
Name: common.PrefixFlag(FlagPrefix, "bls-operator-state-retriever"),
Usage: "Address of the BLS Operator State Retriever",
Required: true,
EnvVar: common.PrefixEnvVar(envPrefix, "BLS_OPERATOR_STATE_RETRIVER"),
}
EigenDAServiceManagerFlag = cli.StringFlag{
Name: common.PrefixFlag(FlagPrefix, "eigenda-service-manager"),
Usage: "Address of the EigenDA Service Manager",
Required: true,
EnvVar: common.PrefixEnvVar(envPrefix, "EIGENDA_SERVICE_MANAGER"),
}
/* Optional Flags*/
BlockNumberFlag = cli.Uint64Flag{
Name: common.PrefixFlag(FlagPrefix, "block-number"),
Usage: "Block number to query state from (default: latest)",
Required: false,
EnvVar: common.PrefixEnvVar(envPrefix, "BLOCK_NUMBER"),
Value: 0,
}
QuorumIDsFlag = cli.StringFlag{
Name: common.PrefixFlag(FlagPrefix, "quorum-ids"),
Usage: "Comma-separated list of quorum IDs to scan (default: 0,1,2)",
Required: false,
EnvVar: common.PrefixEnvVar(envPrefix, "QUORUM_IDS"),
Value: "0,1,2",
}
TopNFlag = cli.UintFlag{
Name: common.PrefixFlag(FlagPrefix, "top"),
Usage: "Show only top N operators by stake",
Required: false,
EnvVar: common.PrefixEnvVar(envPrefix, "TOP"),
Value: 0,
}
)

var requiredFlags = []cli.Flag{
BlsOperatorStateRetrieverFlag,
EigenDAServiceManagerFlag,
}

var optionalFlags = []cli.Flag{
BlockNumberFlag,
QuorumIDsFlag,
TopNFlag,
}

// Flags contains the list of configuration options available to the binary.
var Flags []cli.Flag

func init() {
Flags = append(requiredFlags, optionalFlags...)
Flags = append(Flags, common.LoggerCLIFlags(envPrefix, FlagPrefix)...)
Flags = append(Flags, geth.EthClientFlags(envPrefix)...)
Flags = append(Flags, thegraph.CLIFlags(envPrefix)...)
}
43 changes: 43 additions & 0 deletions tools/quorumscan/quorum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package quorumscan

import (
"math/big"

"github.com/Layr-Labs/eigenda/core"
"github.com/Layr-Labs/eigensdk-go/logging"
)

type QuorumMetrics struct {
Operators []string `json:"operators"`
OperatorStake map[string]float64 `json:"operator_stake"`
OperatorStakePct map[string]float64 `json:"operator_stake_pct"`
}

func QuorumScan(operators map[core.OperatorID]*core.IndexedOperatorInfo, operatorState *core.OperatorState, logger logging.Logger) map[uint8]*QuorumMetrics {
metrics := make(map[uint8]*QuorumMetrics)
for operatorId := range operators {

// Calculate stake percentage for each quorum
for quorum, totalOperatorInfo := range operatorState.Totals {
if _, exists := metrics[quorum]; !exists {
metrics[quorum] = &QuorumMetrics{
Operators: []string{},
OperatorStakePct: make(map[string]float64),
OperatorStake: make(map[string]float64),
}
}
stakePercentage := float64(0)
if stake, ok := operatorState.Operators[quorum][operatorId]; ok {
totalStake := new(big.Float).SetInt(totalOperatorInfo.Stake)
operatorStake := new(big.Float).SetInt(stake.Stake)
stakePercentage, _ = new(big.Float).Mul(big.NewFloat(100), new(big.Float).Quo(operatorStake, totalStake)).Float64()
stakeValue, _ := operatorStake.Float64()
metrics[quorum].Operators = append(metrics[quorum].Operators, operatorId.Hex())
metrics[quorum].OperatorStake[operatorId.Hex()] = stakeValue
metrics[quorum].OperatorStakePct[operatorId.Hex()] = stakePercentage
}
}
}

return metrics
}

0 comments on commit 3bc722b

Please sign in to comment.