Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stake percentage + operator address to ejection reporting (dataapi + cli) #815

Merged
merged 4 commits into from
Oct 28, 2024
Merged
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
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this section of code is highly similar to the codeblock above. Create a new function?

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
Loading