Skip to content

Commit

Permalink
Add stake percentage + operator address to ejection reporting (dataap…
Browse files Browse the repository at this point in the history
…i + cli) (#815)
  • Loading branch information
pschork authored Oct 28, 2024
1 parent 269029f commit 0ec3d07
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 5 deletions.
6 changes: 6 additions & 0 deletions disperser/dataapi/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,12 +992,18 @@ const docTemplate = `{
"block_timestamp": {
"type": "string"
},
"operator_address": {
"type": "string"
},
"operator_id": {
"type": "string"
},
"quorum": {
"type": "integer"
},
"stake_percentage": {
"type": "number"
},
"transaction_hash": {
"type": "string"
}
Expand Down
6 changes: 6 additions & 0 deletions disperser/dataapi/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -988,12 +988,18 @@
"block_timestamp": {
"type": "string"
},
"operator_address": {
"type": "string"
},
"operator_id": {
"type": "string"
},
"quorum": {
"type": "integer"
},
"stake_percentage": {
"type": "number"
},
"transaction_hash": {
"type": "string"
}
Expand Down
4 changes: 4 additions & 0 deletions disperser/dataapi/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@ definitions:
type: integer
block_timestamp:
type: string
operator_address:
type: string
operator_id:
type: string
quorum:
type: integer
stake_percentage:
type: number
transaction_hash:
type: string
type: object
Expand Down
66 changes: 66 additions & 0 deletions disperser/dataapi/queried_operators_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package dataapi
import (
"context"
"fmt"
"math/big"
"net"
"sort"
"strings"
"time"

"github.com/Layr-Labs/eigenda/core"
Expand Down Expand Up @@ -106,6 +108,70 @@ func (s *server) getOperatorEjections(ctx context.Context, days int32, operatorI
return nil, err
}

// create a sorted slice from the set of quorums
quorumSet := make(map[uint8]struct{})
for _, ejection := range operatorEjections {
quorumSet[ejection.Quorum] = struct{}{}
}
quorums := make([]uint8, 0, len(quorumSet))
for quorum := range quorumSet {
quorums = append(quorums, quorum)
}
sort.Slice(quorums, func(i, j int) bool {
return quorums[i] < quorums[j]
})

stateCache := make(map[uint64]*core.OperatorState)
ejectedOperatorIds := make(map[core.OperatorID]struct{})
for _, ejection := range operatorEjections {
previouseBlock := ejection.BlockNumber - 1
if _, exists := stateCache[previouseBlock]; !exists {
state, err := s.chainState.GetOperatorState(context.Background(), uint(previouseBlock), quorums)
if err != nil {
return nil, err
}
stateCache[previouseBlock] = state
}

// construct a set of ejected operator ids for later batch address lookup
opID, err := core.OperatorIDFromHex(ejection.OperatorId)
if err != nil {
return nil, err
}
ejectedOperatorIds[opID] = struct{}{}
}

// resolve operator id to operator addresses mapping
operatorIDs := make([]core.OperatorID, 0, len(ejectedOperatorIds))
for opID := range ejectedOperatorIds {
operatorIDs = append(operatorIDs, opID)
}
operatorAddresses, err := s.transactor.BatchOperatorIDToAddress(ctx, operatorIDs)
if err != nil {
return nil, err
}
operatorIdToAddress := make(map[string]string)
for i := range operatorAddresses {
operatorIdToAddress["0x"+operatorIDs[i].Hex()] = strings.ToLower(operatorAddresses[i].Hex())
}

for _, ejection := range operatorEjections {
state := stateCache[ejection.BlockNumber-1]
opID, err := core.OperatorIDFromHex(ejection.OperatorId)
if err != nil {
return nil, err
}

stakePercentage := float64(0)
if stake, ok := state.Operators[ejection.Quorum][opID]; ok {
totalStake := new(big.Float).SetInt(state.Totals[ejection.Quorum].Stake)
operatorStake := new(big.Float).SetInt(stake.Stake)
stakePercentage, _ = new(big.Float).Mul(big.NewFloat(100), new(big.Float).Quo(operatorStake, totalStake)).Float64()
}
ejection.StakePercentage = stakePercentage
ejection.OperatorAddress = operatorIdToAddress[ejection.OperatorId]
}

s.logger.Info("Get operator ejections", "days", days, "operatorId", operatorId, "len", len(operatorEjections), "duration", time.Since(startTime))
return operatorEjections, nil
}
Expand Down
12 changes: 7 additions & 5 deletions disperser/dataapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,13 @@ type (
}

QueriedOperatorEjections struct {
OperatorId string `json:"operator_id"`
Quorum uint8 `json:"quorum"`
BlockNumber uint64 `json:"block_number"`
BlockTimestamp string `json:"block_timestamp"`
TransactionHash string `json:"transaction_hash"`
OperatorId string `json:"operator_id"`
OperatorAddress string `json:"operator_address"`
Quorum uint8 `json:"quorum"`
BlockNumber uint64 `json:"block_number"`
BlockTimestamp string `json:"block_timestamp"`
TransactionHash string `json:"transaction_hash"`
StakePercentage float64 `json:"stake_percentage"`
}
QueriedOperatorEjectionsResponse struct {
Ejections []*QueriedOperatorEjections `json:"ejections"`
Expand Down
9 changes: 9 additions & 0 deletions tools/ejections/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
build: clean
go mod tidy
go build -o ./bin/ejections ./cmd

clean:
rm -rf ./bin

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

import (
"context"
"errors"
"fmt"
"log"
"math/big"
"os"
"sort"
"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/disperser/dataapi"
"github.com/Layr-Labs/eigenda/disperser/dataapi/subgraph"
"github.com/Layr-Labs/eigenda/tools/ejections"
"github.com/Layr-Labs/eigenda/tools/ejections/flags"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/urfave/cli"

gethcommon "github.com/ethereum/go-ethereum/common"
)

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

type EjectionTransaction struct {
BlockNumber uint64 `json:"block_number"`
BlockTimestamp string `json:"block_timestamp"`
TransactionHash string `json:"transaction_hash"`
QuorumStakePercentage map[uint8]float64 `json:"stake_percentage"`
QuorumEjections map[uint8]uint8 `json:"ejections"`
}

func main() {
app := cli.NewApp()
app.Version = fmt.Sprintf("%s,%s,%s", version, gitCommit, gitDate)
app.Name = "ejections report"
app.Description = "operator ejections report"
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 := ejections.NewConfig(ctx)
if err != nil {
return err
}

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

client, err := geth.NewMultiHomingClient(config.EthClientConfig, gethcommon.Address{}, logger)
if err != nil {
return err
}

tx, err := eth.NewReader(logger, client, config.BLSOperatorStateRetrieverAddr, config.EigenDAServiceManagerAddr)
if err != nil {
return err
}

chainState := eth.NewChainState(tx, client)
if chainState == nil {
return errors.New("failed to create chain state")
}
subgraphApi := subgraph.NewApi(config.SubgraphEndpoint, config.SubgraphEndpoint)
subgraphClient := dataapi.NewSubgraphClient(subgraphApi, logger)

ejections, err := subgraphClient.QueryOperatorEjectionsForTimeWindow(context.Background(), int32(config.Days), config.OperatorId, config.First, config.Skip)
if err != nil {
logger.Warn("failed to fetch operator ejections", "operatorId", config.OperatorId, "error", err)
return errors.New("operator ejections not found")
}

sort.Slice(ejections, func(i, j int) bool {
return ejections[i].BlockTimestamp > ejections[j].BlockTimestamp
})

// Create a sorted slice from the set of quorums
quorumSet := make(map[uint8]struct{})
for _, ejection := range ejections {
quorumSet[ejection.Quorum] = struct{}{}
}
quorums := make([]uint8, 0, len(quorumSet))
for quorum := range quorumSet {
quorums = append(quorums, quorum)
}
sort.Slice(quorums, func(i, j int) bool {
return quorums[i] < quorums[j]
})

stateCache := make(map[uint64]*core.OperatorState)
ejectedOperatorIds := make(map[core.OperatorID]struct{})
for _, ejection := range ejections {
previouseBlock := ejection.BlockNumber - 1
if _, exists := stateCache[previouseBlock]; !exists {
state, err := chainState.GetOperatorState(context.Background(), uint(previouseBlock), quorums)
if err != nil {
return err
}
stateCache[previouseBlock] = state
}

// construct a set of ejected operator ids for later batch address lookup
opID, err := core.OperatorIDFromHex(ejection.OperatorId)
if err != nil {
return err
}
ejectedOperatorIds[opID] = struct{}{}
}

// resolve operator id to operator addresses mapping
operatorIDs := make([]core.OperatorID, 0, len(ejectedOperatorIds))
for opID := range ejectedOperatorIds {
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["0x"+operatorIDs[i].Hex()] = strings.ToLower(operatorAddresses[i].Hex())
}

rowConfigAutoMerge := table.RowConfig{AutoMerge: true}
rowConfigNoAutoMerge := table.RowConfig{AutoMerge: false}
operators := table.NewWriter()
operators.AppendHeader(table.Row{"Operator Address", "Quorum", "Stake %", "Timestamp", "Txn"}, rowConfigAutoMerge)
txns := table.NewWriter()
txns.AppendHeader(table.Row{"Txn", "Timestamp", "Operator Address", "Quorum", "Stake %"}, rowConfigAutoMerge)
txnQuorums := table.NewWriter()
txnQuorums.AppendHeader(table.Row{"Txn", "Timestamp", "Quorum", "Stake %", "Operators"}, rowConfigNoAutoMerge)

ejectionTransactions := make(map[string]*EjectionTransaction)
for _, ejection := range ejections {
state := stateCache[ejection.BlockNumber-1]
opID, err := core.OperatorIDFromHex(ejection.OperatorId)
if err != nil {
return err
}

stakePercentage := float64(0)
if stake, ok := state.Operators[ejection.Quorum][opID]; ok {
totalStake := new(big.Float).SetInt(state.Totals[ejection.Quorum].Stake)
operatorStake := new(big.Float).SetInt(stake.Stake)
stakePercentage, _ = new(big.Float).Mul(big.NewFloat(100), new(big.Float).Quo(operatorStake, totalStake)).Float64()
}

if _, exists := ejectionTransactions[ejection.TransactionHash]; !exists {
ejectionTransactions[ejection.TransactionHash] = &EjectionTransaction{
BlockNumber: ejection.BlockNumber,
BlockTimestamp: ejection.BlockTimestamp,
TransactionHash: ejection.TransactionHash,
QuorumStakePercentage: make(map[uint8]float64),
QuorumEjections: make(map[uint8]uint8),
}
ejectionTransactions[ejection.TransactionHash].QuorumStakePercentage[ejection.Quorum] = stakePercentage
ejectionTransactions[ejection.TransactionHash].QuorumEjections[ejection.Quorum] = 1
} else {
ejectionTransactions[ejection.TransactionHash].QuorumStakePercentage[ejection.Quorum] += stakePercentage
ejectionTransactions[ejection.TransactionHash].QuorumEjections[ejection.Quorum] += 1
}

operatorAddress := operatorIdToAddress[ejection.OperatorId]
operators.AppendRow(table.Row{operatorAddress, ejection.Quorum, stakePercentage, ejection.BlockTimestamp, ejection.TransactionHash}, rowConfigAutoMerge)
txns.AppendRow(table.Row{ejection.TransactionHash, ejection.BlockTimestamp, operatorAddress, ejection.Quorum, stakePercentage}, rowConfigAutoMerge)
}

orderedEjectionTransactions := make([]*EjectionTransaction, 0, len(ejectionTransactions))
for _, txn := range ejectionTransactions {
orderedEjectionTransactions = append(orderedEjectionTransactions, txn)
}
sort.Slice(orderedEjectionTransactions, func(i, j int) bool {
return orderedEjectionTransactions[i].BlockNumber > orderedEjectionTransactions[j].BlockNumber
})
for _, txn := range orderedEjectionTransactions {
for _, quorum := range quorums {
if _, exists := txn.QuorumEjections[quorum]; exists {
txnQuorums.AppendRow(table.Row{txn.TransactionHash, txn.BlockTimestamp, quorum, txn.QuorumStakePercentage[quorum], txn.QuorumEjections[quorum]}, rowConfigAutoMerge)
}
}
}

operators.SetAutoIndex(true)
operators.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, Align: text.AlignCenter},
})
operators.SetStyle(table.StyleLight)
operators.Style().Options.SeparateRows = true

txns.SetAutoIndex(true)
txns.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, AutoMerge: true},
{Number: 3, AutoMerge: true},
{Number: 4, Align: text.AlignCenter},
})
txns.SetStyle(table.StyleLight)
txns.Style().Options.SeparateRows = true

txnQuorums.SetAutoIndex(true)
txnQuorums.SetColumnConfigs([]table.ColumnConfig{
{Number: 1, AutoMerge: true},
{Number: 2, AutoMerge: true, Align: text.AlignCenter},
{Number: 3, Align: text.AlignCenter},
{Number: 5, Align: text.AlignCenter},
})
txnQuorums.SetStyle(table.StyleLight)
txnQuorums.Style().Options.SeparateRows = true

fmt.Println(operators.Render())
fmt.Println(txns.Render())
fmt.Println(txnQuorums.Render())
return nil
}
Loading

0 comments on commit 0ec3d07

Please sign in to comment.