diff --git a/cli/commands/checkpoint.go b/cli/commands/checkpoint.go index 8beb62c9..c0b27eaf 100644 --- a/cli/commands/checkpoint.go +++ b/cli/commands/checkpoint.go @@ -5,6 +5,7 @@ import ( "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core/onchain" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/utils" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -94,7 +95,7 @@ func CheckpointCommand(args TCheckpointCommandArgs) error { txns, err := core.SubmitCheckpointProof(ctx, args.Sender, args.EigenpodAddress, chainId, proof, eth, args.BatchSize, args.NoPrompt, args.SimulateTransaction) if args.SimulateTransaction { - printableTxns := aMap(txns, func(txn *types.Transaction) Transaction { + printableTxns := utils.Map(txns, func(txn *types.Transaction, _ uint64) Transaction { return Transaction{ To: txn.To().Hex(), CallData: common.Bytes2Hex(txn.Data()), diff --git a/cli/commands/credentials.go b/cli/commands/credentials.go index c445cec0..cc230af8 100644 --- a/cli/commands/credentials.go +++ b/cli/commands/credentials.go @@ -7,6 +7,7 @@ import ( "math/big" "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/fatih/color" @@ -65,7 +66,7 @@ func CredentialsCommand(args TCredentialCommandArgs) error { }()), err) if args.SimulateTransaction { - out := aMap(txns, func(txn *types.Transaction) CredentialProofTransaction { + out := utils.Map(txns, func(txn *types.Transaction, _ uint64) CredentialProofTransaction { gas := txn.Gas() return CredentialProofTransaction{ Transaction: Transaction{ @@ -79,7 +80,7 @@ func CredentialsCommand(args TCredentialCommandArgs) error { return nil }(), }, - ValidatorIndices: aMap(aFlatten(indices), func(index *big.Int) uint64 { + ValidatorIndices: utils.Map(utils.Flatten(indices), func(index *big.Int, _ uint64) uint64 { return index.Uint64() }), } diff --git a/cli/commands/staleBalance.go b/cli/commands/staleBalance.go index d7382132..1e55dfaf 100644 --- a/cli/commands/staleBalance.go +++ b/cli/commands/staleBalance.go @@ -18,7 +18,7 @@ type TFixStaleBalanceArgs struct { BeaconNode string Sender string EigenpodAddress string - SlashedValidatorIndex int64 + SlashedValidatorIndex uint64 Verbose bool CheckpointBatchSize uint64 NoPrompt bool @@ -39,7 +39,7 @@ func FixStaleBalance(args TFixStaleBalanceArgs) error { eth, beacon, chainId, err := core.GetClients(ctx, args.EthNode, args.BeaconNode, args.Verbose) core.PanicOnError("failed to get clients", err) - validator, err := beacon.GetValidator(ctx, uint64(args.SlashedValidatorIndex)) + validator, err := beacon.GetValidator(ctx, args.SlashedValidatorIndex) core.PanicOnError("failed to fetch validator state", err) if !validator.Validator.Slashed { @@ -75,7 +75,7 @@ func FixStaleBalance(args TFixStaleBalanceArgs) error { } } - proof, oracleBeaconTimesetamp, err := core.GenerateValidatorProof(ctx, args.EigenpodAddress, eth, chainId, beacon, new(big.Int).SetUint64(uint64(args.SlashedValidatorIndex)), args.Verbose) + proof, oracleBeaconTimesetamp, err := core.GenerateValidatorProof(ctx, args.EigenpodAddress, eth, chainId, beacon, new(big.Int).SetUint64(args.SlashedValidatorIndex), args.Verbose) core.PanicOnError("failed to generate credential proof for slashed validator", err) if !args.NoPrompt { diff --git a/cli/commands/status.go b/cli/commands/status.go index 701ba539..bb9b90a2 100644 --- a/cli/commands/status.go +++ b/cli/commands/status.go @@ -74,7 +74,7 @@ func StatusCommand(args TStatusArgs) error { for _, validator := range awaitingActivationQueueValidators { publicKey := validator.PublicKey if !isVerbose { - publicKey = shortenHex(publicKey) + publicKey = utils.ShortenHex(publicKey) } targetColor = color.New(color.FgHiRed) @@ -97,7 +97,7 @@ func StatusCommand(args TStatusArgs) error { for _, validator := range inactiveValidators { publicKey := validator.PublicKey if !isVerbose { - publicKey = shortenHex(publicKey) + publicKey = utils.ShortenHex(publicKey) } if validator.Slashed { @@ -120,7 +120,7 @@ func StatusCommand(args TStatusArgs) error { for _, validator := range activeValidators { publicKey := validator.PublicKey if !isVerbose { - publicKey = shortenHex(publicKey) + publicKey = utils.ShortenHex(publicKey) } if validator.Slashed { @@ -143,7 +143,7 @@ func StatusCommand(args TStatusArgs) error { for _, validator := range withdrawnValidators { publicKey := validator.PublicKey if !isVerbose { - publicKey = shortenHex(publicKey) + publicKey = utils.ShortenHex(publicKey) } if validator.Slashed { diff --git a/cli/commands/utils.go b/cli/commands/utils.go index f0f8fbbb..49059b16 100644 --- a/cli/commands/utils.go +++ b/cli/commands/utils.go @@ -25,24 +25,3 @@ func printProofs(txns any) { core.PanicOnError("failed to serialize proofs", err) fmt.Println(string(out)) } - -// imagine if golang had a standard library -func aMap[A any, B any](coll []A, mapper func(i A) B) []B { - out := make([]B, len(coll)) - for i, item := range coll { - out[i] = mapper(item) - } - return out -} - -func aFlatten[A any](coll [][]A) []A { - out := []A{} - for _, arr := range coll { - out = append(out, arr...) - } - return out -} - -func shortenHex(publicKey string) string { - return publicKey[0:6] + ".." + publicKey[len(publicKey)-4:] -} diff --git a/cli/core/findStalePods.go b/cli/core/findStalePods.go index 52f7f662..3a92d4c3 100644 --- a/cli/core/findStalePods.go +++ b/cli/core/findStalePods.go @@ -5,12 +5,15 @@ import ( "fmt" "log" "math/big" + "strings" "github.com/Layr-Labs/eigenpod-proofs-generation/cli/core/onchain" + "github.com/Layr-Labs/eigenpod-proofs-generation/cli/utils" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" "github.com/pkg/errors" ) @@ -21,18 +24,34 @@ func PodManagerContracts() map[uint64]string { } } +func weiToGwei(val uint64) phase0.Gwei { + return phase0.Gwei(new(big.Int).Div(new(big.Int).SetUint64(val), big.NewInt(params.GWei)).Uint64()) +} + type Cache struct { PodOwnerShares map[string]PodOwnerShare } +func keys[A comparable, B any](coll map[A]B) []A { + if len(coll) == 0 { + return []A{} + } + out := make([]A, len(coll)) + for key, _ := range coll { + out[len(out)] = key + } + return out +} + type PodOwnerShare struct { - Shares uint64 - IsEigenpod bool + SharesWei uint64 + ExecutionLayerBalanceWei uint64 + IsEigenpod bool } const ACCEPTABLE_BALANCE_DEVIATION = float64(0.95) -var cache Cache +var cache Cache // valid for the duration of a command. func isEigenpod(eth *ethclient.Client, chainId uint64, eigenpodAddress string) (bool, error) { if cache.PodOwnerShares == nil { @@ -45,8 +64,9 @@ func isEigenpod(eth *ethclient.Client, chainId uint64, eigenpodAddress string) ( // default to false cache.PodOwnerShares[eigenpodAddress] = PodOwnerShare{ - Shares: 0, - IsEigenpod: false, + SharesWei: 0, + ExecutionLayerBalanceWei: 0, + IsEigenpod: false, } podManAddress, ok := PodManagerContracts()[chainId] @@ -85,11 +105,17 @@ func isEigenpod(eth *ethclient.Client, chainId uint64, eigenpodAddress string) ( return false, fmt.Errorf("PodOwnerShares() failed: %s", err.Error()) } + balance, err := eth.BalanceAt(context.Background(), common.HexToAddress(eigenpodAddress), nil) + if err != nil { + return false, fmt.Errorf("balance check failed: %s", err.Error()) + } + // Simulate fetching from contracts // Implement contract fetching logic here cache.PodOwnerShares[eigenpodAddress] = PodOwnerShare{ - Shares: podOwnerShares.Uint64(), - IsEigenpod: true, + SharesWei: podOwnerShares.Uint64(), + ExecutionLayerBalanceWei: balance.Uint64(), + IsEigenpod: true, } return true, nil @@ -103,24 +129,6 @@ func executionWithdrawalAddress(withdrawalCredentials []byte) *string { return &addr } -func aFilter[T any](coll []T, criteria func(T) bool) []T { - var result []T - for _, item := range coll { - if criteria(item) { - result = append(result, item) - } - } - return result -} - -func aMap[T any, A any](coll []T, mapper func(T, uint64) A) []A { - var result []A - for idx, item := range coll { - result = append(result, mapper(item, uint64(idx))) - } - return result -} - func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl string, beacon BeaconClient, chainId *big.Int, verbose bool) (map[string][]ValidatorWithIndex, error) { beaconState, err := beacon.GetBeaconState(ctx, "head") if err != nil { @@ -133,7 +141,7 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri return nil, err } - allValidatorsWithIndices := aMap(_allValidators, func(validator *phase0.Validator, index uint64) ValidatorWithIndex { + allValidatorsWithIndices := utils.Map(_allValidators, func(validator *phase0.Validator, index uint64) ValidatorWithIndex { return ValidatorWithIndex{ Validator: validator, Index: index, @@ -141,7 +149,7 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri }) // TODO(pectra): this logic changes after the pectra upgrade. - allSlashedValidators := aFilter(allValidatorsWithIndices, func(v ValidatorWithIndex) bool { + allSlashedValidators := utils.Filter(allValidatorsWithIndices, func(v ValidatorWithIndex) bool { if !v.Validator.Slashed { return false // we only care about slashed validators. } @@ -161,9 +169,7 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri return map[string][]ValidatorWithIndex{}, nil } - validatorToPod := map[uint64]string{} - - allSlashedValidatorsBelongingToEigenpods := aFilter(allSlashedValidators, func(validator ValidatorWithIndex) bool { + allSlashedValidatorsBelongingToEigenpods := utils.Filter(allSlashedValidators, func(validator ValidatorWithIndex) bool { isPod, err := isEigenpod(eth, chainId.Uint64(), *executionWithdrawalAddress(validator.Validator.WithdrawalCredentials)) if err != nil { return false @@ -172,11 +178,13 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri }) allValidatorInfo := make(map[uint64]onchain.IEigenPodValidatorInfo) - for _, validator := range allSlashedValidatorsBelongingToEigenpods { eigenpodAddress := *executionWithdrawalAddress(validator.Validator.WithdrawalCredentials) pod, err := onchain.NewEigenPod(common.HexToAddress(eigenpodAddress), eth) - PanicOnError("failed to dial eigenpod", err) + if err != nil { + // failed to load validator info. + return map[string][]ValidatorWithIndex{}, fmt.Errorf("failed to dial eigenpod: %s", err.Error()) + } info, err := pod.ValidatorPubkeyToInfo(nil, validator.Validator.PublicKey[:]) if err != nil { @@ -186,48 +194,69 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri allValidatorInfo[validator.Index] = info } - allActiveSlashedValidatorsBelongingToEigenpods := aFilter(allSlashedValidatorsBelongingToEigenpods, func(validator ValidatorWithIndex) bool { + allActiveSlashedValidatorsBelongingToEigenpods := utils.Filter(allSlashedValidatorsBelongingToEigenpods, func(validator ValidatorWithIndex) bool { validatorInfo := allValidatorInfo[validator.Index] - return validatorInfo.Status == 1 + return validatorInfo.Status == 1 // "ACTIVE" }) if verbose { log.Printf("%d EigenValidators were slashed\n", len(allActiveSlashedValidatorsBelongingToEigenpods)) } - slashedEigenpods := make(map[string][]ValidatorWithIndex) - for _, validator := range allActiveSlashedValidatorsBelongingToEigenpods { + slashedEigenpods := utils.Reduce(allActiveSlashedValidatorsBelongingToEigenpods, func(pods map[string][]ValidatorWithIndex, validator ValidatorWithIndex) map[string][]ValidatorWithIndex { podAddress := executionWithdrawalAddress(validator.Validator.WithdrawalCredentials) if podAddress != nil { - slashedEigenpods[*podAddress] = append(slashedEigenpods[*podAddress], validator) - validatorToPod[validator.Index] = *podAddress + if pods[*podAddress] == nil { + pods[*podAddress] = []ValidatorWithIndex{} + } + pods[*podAddress] = append(pods[*podAddress], validator) } - } - - if verbose { - log.Printf("%d EigenPods were slashed\n", len(slashedEigenpods)) - } + return pods + }, map[string][]ValidatorWithIndex{}) allValidatorBalances, err := beaconState.ValidatorBalances() if err != nil { return nil, err } - var unhealthyEigenpods map[string]bool = make(map[string]bool) - for _, validator := range allActiveSlashedValidatorsBelongingToEigenpods { - balance := allValidatorBalances[validator.Index] - pod := validatorToPod[validator.Index] - executionBalance := cache.PodOwnerShares[pod].Shares - if executionBalance == 0 { - continue + totalAssetsWeiByEigenpod := utils.Reduce(keys(slashedEigenpods), func(allBalances map[string]uint64, eigenpod string) map[string]uint64 { + // total assets of an eigenpod are determined as; + // SUM( + // - native ETH in the pod + // - any active validators and their associated balances + // ) + allValidatorsForEigenpod := utils.Filter(allValidatorsWithIndices, func(v ValidatorWithIndex) bool { + withdrawal := executionWithdrawalAddress(v.Validator.WithdrawalCredentials) + return withdrawal != nil && strings.EqualFold(*withdrawal, eigenpod) + }) + + allValidatorBalancesSummedGwei := utils.Reduce(allValidatorsForEigenpod, func(accum phase0.Gwei, validator ValidatorWithIndex) phase0.Gwei { + return accum + allValidatorBalances[validator.Index] + }, phase0.Gwei(0)) + // converting gwei to wei + allBalances[eigenpod] = cache.PodOwnerShares[eigenpod].ExecutionLayerBalanceWei + (uint64(allValidatorBalancesSummedGwei) * params.GWei) + return allBalances + }, map[string]uint64{}) + + if verbose { + log.Printf("%d EigenPods were slashed\n", len(slashedEigenpods)) + } + + unhealthyEigenpods := utils.Filter(keys(slashedEigenpods), func(eigenpod string) bool { + balance, ok := totalAssetsWeiByEigenpod[eigenpod] + if !ok { + return false } - if balance <= phase0.Gwei(float64(executionBalance)*ACCEPTABLE_BALANCE_DEVIATION) { - unhealthyEigenpods[pod] = true + executionBalance := cache.PodOwnerShares[eigenpod].SharesWei + if balance <= uint64(float64(executionBalance)*ACCEPTABLE_BALANCE_DEVIATION) { if verbose { - log.Printf("[%s] %.2f%% deviation (beacon: %d -> execution: %d)\n", pod, 100*(float64(executionBalance)-float64(balance))/float64(executionBalance), balance, executionBalance) + log.Printf("[%s] %.2f%% deviation (beacon: %d -> execution: %d)\n", eigenpod, 100*(float64(executionBalance)-float64(balance))/float64(executionBalance), balance, executionBalance) } + return true } - } + + return false + }) if len(unhealthyEigenpods) == 0 { if verbose { @@ -241,7 +270,7 @@ func FindStaleEigenpods(ctx context.Context, eth *ethclient.Client, nodeUrl stri } var entries map[string][]ValidatorWithIndex = make(map[string][]ValidatorWithIndex) - for val := range unhealthyEigenpods { + for _, val := range unhealthyEigenpods { entries[val] = slashedEigenpods[val] } diff --git a/cli/main.go b/cli/main.go index 33bc0bdd..82ef84d4 100644 --- a/cli/main.go +++ b/cli/main.go @@ -71,7 +71,7 @@ func main() { BeaconNode: beacon, Sender: sender, EigenpodAddress: eigenpodAddress, - SlashedValidatorIndex: int64(slashedValidatorIndex), + SlashedValidatorIndex: slashedValidatorIndex, Verbose: verbose, CheckpointBatchSize: batchSize, NoPrompt: noPrompt, diff --git a/cli/utils/utils.go b/cli/utils/utils.go index 341d348f..c84626ed 100644 --- a/cli/utils/utils.go +++ b/cli/utils/utils.go @@ -1,9 +1,44 @@ package utils -func ShortenHex(publicKey string) string { - return publicKey[0:6] + ".." + publicKey[len(publicKey)-4:] -} - // maximum number of proofs per txn for each of the following proof types: const DEFAULT_BATCH_CREDENTIALS = 60 const DEFAULT_BATCH_CHECKPOINT = 80 + +// imagine if golang had a standard library +func Map[A any, B any](coll []A, mapper func(i A, index uint64) B) []B { + out := make([]B, len(coll)) + for i, item := range coll { + out[i] = mapper(item, uint64(i)) + } + return out +} + +func Filter[A any](coll []A, criteria func(i A) bool) []A { + out := []A{} + for _, item := range coll { + if criteria(item) { + out = append(out, item) + } + } + return out +} + +func Reduce[A any, B any](coll []A, processor func(accum B, next A) B, initialState B) B { + val := initialState + for _, item := range coll { + val = processor(val, item) + } + return val +} + +func Flatten[A any](coll [][]A) []A { + out := []A{} + for _, arr := range coll { + out = append(out, arr...) + } + return out +} + +func ShortenHex(publicKey string) string { + return publicKey[0:6] + ".." + publicKey[len(publicKey)-4:] +}