-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Quorum operator stake report cli (#1162)
- Loading branch information
Showing
5 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |