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

Support multicall3 in status endpoint #151

Merged
merged 2 commits into from
Sep 6, 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
2 changes: 1 addition & 1 deletion cli/core/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func GenerateCheckpointProofForState(ctx context.Context, eigenpodAddress string
color.Yellow("You have a total of %d validators pointed to this pod.", len(allValidators))
}

allValidatorsWithInfo, err := FetchMultipleOnchainValidatorInfo(eth, eigenpodAddress, allValidators)
allValidatorsWithInfo, err := FetchMultipleOnchainValidatorInfo(ctx, eth, eigenpodAddress, allValidators)
if err != nil {
return nil, err
}
Expand Down
229 changes: 229 additions & 0 deletions cli/core/multicall/multicall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package multicall

import (
"context"
"errors"
"fmt"
"math"
"strings"

"github.com/Layr-Labs/eigenpod-proofs-generation/cli/utils"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)

type MultiCallMetaData[T interface{}] struct {
Address common.Address
Data []byte
Deserialize func([]byte) (T, error)
}

type Multicall3Result struct {
Success bool
ReturnData []byte
}

type DeserializedMulticall3Result struct {
Success bool
Value any
}

func (md *MultiCallMetaData[T]) Raw() RawMulticall {
return RawMulticall{
Address: md.Address,
Data: md.Data,
Deserialize: func(data []byte) (any, error) {
res, err := md.Deserialize(data)
return any(res), err
},
}
}

type RawMulticall struct {
Address common.Address
Data []byte
Deserialize func([]byte) (any, error)
}

type MulticallContract struct {
Contract *bind.BoundContract
ABI *abi.ABI
Context context.Context
MaxBatchSize uint64
}

type ParamMulticall3Call3 struct {
Target common.Address
AllowFailure bool
CallData []byte
}

// maxBatchSizeBytes - 0: no batching.
func NewMulticallContract(ctx context.Context, eth *ethclient.Client, address *common.Address, maxBatchSizeBytes uint64) (*MulticallContract, error) {
if eth == nil {
return nil, errors.New("no ethclient passed")
}

// taken from: https://www.multicall3.com/
parsed, err := abi.JSON(strings.NewReader(`[{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"aggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes[]","name":"returnData","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3[]","name":"calls","type":"tuple[]"}],"name":"aggregate3","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bool","name":"allowFailure","type":"bool"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call3Value[]","name":"calls","type":"tuple[]"}],"name":"aggregate3Value","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"blockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"getBasefee","outputs":[{"internalType":"uint256","name":"basefee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"name":"getBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBlockNumber","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainId","outputs":[{"internalType":"uint256","name":"chainid","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockCoinbase","outputs":[{"internalType":"address","name":"coinbase","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockDifficulty","outputs":[{"internalType":"uint256","name":"difficulty","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockGasLimit","outputs":[{"internalType":"uint256","name":"gaslimit","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getLastBlockHash","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryAggregate","outputs":[{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bool","name":"requireSuccess","type":"bool"},{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct Multicall3.Call[]","name":"calls","type":"tuple[]"}],"name":"tryBlockAndAggregate","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct Multicall3.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"payable","type":"function"}]`))
if err != nil {
return nil, fmt.Errorf("error parsing multicall abi: %s", err.Error())
}

contractAddress := func() common.Address {
if address == nil {
// also taken from: https://www.multicall3.com/ -- it's deployed at the same addr on most chains
return common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")
}
return *address
}()

return &MulticallContract{MaxBatchSize: maxBatchSizeBytes, Context: ctx, ABI: &parsed, Contract: bind.NewBoundContract(contractAddress, parsed, eth, eth, eth)}, nil
}

// Call invokes the (constant) contract method with params as input values and
// sets the output to result. The result type might be a single field for simple
// returns, a slice of interfaces for anonymous returns and a struct for named
// returns.
func MultiCall[T any](contractAddress common.Address, abi abi.ABI, deserialize func([]byte) (T, error), method string, params ...interface{}) (*MultiCallMetaData[T], error) {
callData, err := abi.Pack(method, params...)
if err != nil {
return nil, fmt.Errorf("error packing multicall: %s", err.Error())
}
return &MultiCallMetaData[T]{
Address: contractAddress,
Data: callData,
Deserialize: deserialize,
}, nil
}

func DoMultiCall[A any, B any](mc MulticallContract, a *MultiCallMetaData[A], b *MultiCallMetaData[B]) (*A, *B, error) {
res, err := doMultiCallMany(mc, a.Raw(), b.Raw())
if err != nil {
return nil, nil, fmt.Errorf("error performing multicall: %s", err.Error())
}
return any(res[0].Value).(*A), any(res[1].Value).(*B), nil
}

func DoMultiCallMany[A any](mc MulticallContract, requests ...*MultiCallMetaData[A]) (*[]A, error) {
res, err := doMultiCallMany(mc, utils.Map(requests, func(mc *MultiCallMetaData[A], index uint64) RawMulticall {
return mc.Raw()
})...)
if err != nil {
return nil, fmt.Errorf("multicall failed: %s", err.Error())
}

// unwind results
unwoundResults := utils.Map(res, func(d DeserializedMulticall3Result, i uint64) A {
// force these back to A
return any(d.Value).(A)
})
return &unwoundResults, nil
}

/*
* Some RPC providers may limit the amount of calldata you can send in one eth_call, which (for those who have 1000's of validators), means
* you can't just spam one enormous multicall request.
*
* This function checks whether the calldata appended exceeds maxBatchSizeBytes
*/
func chunkCalls(allCalls []ParamMulticall3Call3, maxBatchSizeBytes int) [][]ParamMulticall3Call3 {
// chunk by the maximum size of calldata, which is 1024 per call.
results := [][]ParamMulticall3Call3{}
currentBatchSize := 0
currentBatch := []ParamMulticall3Call3{}

for _, call := range allCalls {
if (currentBatchSize + len(call.CallData)) > maxBatchSizeBytes {
// we can't fit in this batch, so dump the current batch and start a new one
results = append(results, currentBatch)
currentBatchSize = 0
currentBatch = []ParamMulticall3Call3{}
}

currentBatch = append(currentBatch, call)
currentBatchSize += len(call.CallData)
}

// check if we forgot to add the last batch
if len(currentBatch) > 0 {
results = append(results, currentBatch)
}

return results
}

func doMultiCallMany(mc MulticallContract, calls ...RawMulticall) ([]DeserializedMulticall3Result, error) {
typedCalls := make([]ParamMulticall3Call3, len(calls))
for i, call := range calls {
typedCalls[i] = ParamMulticall3Call3{
Target: call.Address,
AllowFailure: true,
CallData: call.Data,
}
}

// see if we need to chunk them now
chunkedCalls := chunkCalls(typedCalls, func() int {
if mc.MaxBatchSize == 0 {
return math.MaxInt64
} else {
return int(mc.MaxBatchSize)
}
}())
var results = make([]interface{}, len(calls))
var totalResults = 0
jbrower95 marked this conversation as resolved.
Show resolved Hide resolved

for _, multicalls := range chunkedCalls {
var res []interface{}

err := mc.Contract.Call(&bind.CallOpts{}, &res, "aggregate3", multicalls)
if err != nil {
return nil, fmt.Errorf("aggregate3 failed: %s", err)
}

multicallResults := *abi.ConvertType(res[0], new([]Multicall3Result)).(*[]Multicall3Result)

// copy over into master results list
for i := 0; i < len(multicallResults); i++ {
results[totalResults+i] = multicallResults[i]
}
totalResults += len(multicallResults)
}

// now we should have a bunch of Multicall3Result
outputs := make([]DeserializedMulticall3Result, len(calls))
for i, call := range calls {
res := results[i].(Multicall3Result)
if res.Success {
if res.ReturnData != nil {
val, err := call.Deserialize(res.ReturnData)
if err != nil {
outputs[i] = DeserializedMulticall3Result{
Value: err,
Success: false,
}
} else {
outputs[i] = DeserializedMulticall3Result{
Value: val,
Success: res.Success,
}
}
} else {
outputs[i] = DeserializedMulticall3Result{
Value: errors.New("no data returned"),
Success: false,
}
}
} else {
outputs[i] = DeserializedMulticall3Result{
Success: false,
Value: errors.New("call failed"),
}
}
}

return outputs, nil
}
2 changes: 1 addition & 1 deletion cli/core/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func GetStatus(ctx context.Context, eigenpodAddress string, eth *ethclient.Clien
allValidatorsForEigenpod, err := FindAllValidatorsForEigenpod(eigenpodAddress, state)
PanicOnError("failed to find validators", err)

allValidatorsWithInfoForEigenpod, err := FetchMultipleOnchainValidatorInfo(eth, eigenpodAddress, allValidatorsForEigenpod)
allValidatorsWithInfoForEigenpod, err := FetchMultipleOnchainValidatorInfo(ctx, eth, eigenpodAddress, allValidatorsForEigenpod)
PanicOnError("failed to fetch validator info", err)

allBeaconBalances := getRegularBalancesGwei(state)
Expand Down
83 changes: 64 additions & 19 deletions cli/core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import (
"os"
"sort"
"strconv"
"strings"

eigenpodproofs "github.com/Layr-Labs/eigenpod-proofs-generation"
"github.com/Layr-Labs/eigenpod-proofs-generation/cli/core/multicall"
"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"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -280,37 +284,78 @@ func FindAllValidatorsForEigenpod(eigenpodAddress string, beaconState *spec.Vers
return outputValidators, nil
}

func FetchMultipleOnchainValidatorInfo(client *ethclient.Client, eigenpodAddress string, allValidators []ValidatorWithIndex) ([]ValidatorWithOnchainInfo, error) {
eigenPod, err := onchain.NewEigenPod(common.HexToAddress(eigenpodAddress), client)
var zeroes = [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}

func FetchMultipleOnchainValidatorInfo(ctx context.Context, client *ethclient.Client, eigenpodAddress string, allValidators []ValidatorWithIndex) ([]ValidatorWithOnchainInfo, error) {
eigenpodAbi, err := abi.JSON(strings.NewReader(onchain.EigenPodABI))
if err != nil {
return nil, fmt.Errorf("failed to locate Eigenpod. Is your address correct?: %w", err)
return nil, fmt.Errorf("failed to load eigenpod abi: %s", err)
}

var validators []ValidatorWithOnchainInfo = []ValidatorWithOnchainInfo{}
type MulticallAndError struct {
Multicall *multicall.MultiCallMetaData[*onchain.IEigenPodValidatorInfo]
Error error
}

// TODO: batch/multicall
zeroes := [16]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
for i := 0; i < len(allValidators); i++ {
// ssz requires values to be 32-byte aligned, which requires 16 bytes of 0's to be added
// prior to hashing.
requests := utils.Map(allValidators, func(validator ValidatorWithIndex, index uint64) MulticallAndError {
pubKeyHash := sha256.Sum256(
append(
(allValidators[i]).Validator.PublicKey[:],
validator.Validator.PublicKey[:],
zeroes[:]...,
),
)
info, err := eigenPod.ValidatorPubkeyHashToInfo(nil, pubKeyHash)
if err != nil {
return nil, fmt.Errorf("failed to fetch validator eigeninfo: %w", err)

mc, err := multicall.MultiCall(common.HexToAddress(eigenpodAddress), eigenpodAbi, func(data []byte) (*onchain.IEigenPodValidatorInfo, error) {
res, err := eigenpodAbi.Unpack("validatorPubkeyHashToInfo", data)
if err != nil {
return nil, err
}
return abi.ConvertType(res[0], new(onchain.IEigenPodValidatorInfo)).(*onchain.IEigenPodValidatorInfo), nil
}, "validatorPubkeyHashToInfo", pubKeyHash)

return MulticallAndError{
Multicall: mc,
Error: err,
}
})

errs := []error{}
for _, mc := range requests {
if mc.Error != nil {
errs = append(errs, mc.Error)
}
validators = append(validators, ValidatorWithOnchainInfo{
Index: allValidators[i].Index,
Validator: allValidators[i].Validator,
Info: info,
})
}

return validators, nil
if len(errs) > 0 {
return nil, fmt.Errorf("failed to form request for validator info: %s", errors.Join(errs...))
}

allMulticalls := utils.Map(requests, func(mc MulticallAndError, _ uint64) *multicall.MultiCallMetaData[*onchain.IEigenPodValidatorInfo] {
return mc.Multicall
})

// make the multicall requests
multicallInstance, err := multicall.NewMulticallContract(ctx, client, nil, 4096 /* no batching */)
if err != nil {
return nil, fmt.Errorf("failed to contact multicall: %s", err.Error())
}

results, err := multicall.DoMultiCallMany(*multicallInstance, allMulticalls...)
if err != nil {
return nil, fmt.Errorf("failed to fetch validator info: %s", err.Error())
}

if results == nil {
return nil, errors.New("no results returned fetching validator info")
}

return utils.Map(*results, func(info *onchain.IEigenPodValidatorInfo, i uint64) ValidatorWithOnchainInfo {
return ValidatorWithOnchainInfo{
Info: *info,
Validator: allValidators[i].Validator,
Index: allValidators[i].Index,
}
}), nil
}

func GetCurrentCheckpointBlockRoot(eigenpodAddress string, eth *ethclient.Client) (*[32]byte, error) {
Expand Down
6 changes: 3 additions & 3 deletions cli/core/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ func GenerateValidatorProof(ctx context.Context, eigenpodAddress string, eth *et
return nil, 0, fmt.Errorf("failed to initialize provider: %w", err)
}

proofs, err := GenerateValidatorProofAtState(proofExecutor, eigenpodAddress, beaconState, eth, chainId, header, latestBlock.Time(), validatorIndex, verbose)
proofs, err := GenerateValidatorProofAtState(ctx, proofExecutor, eigenpodAddress, beaconState, eth, chainId, header, latestBlock.Time(), validatorIndex, verbose)
return proofs, latestBlock.Time(), err
}

func GenerateValidatorProofAtState(proofs *eigenpodproofs.EigenPodProofs, eigenpodAddress string, beaconState *spec.VersionedBeaconState, eth *ethclient.Client, chainId *big.Int, header *v1.BeaconBlockHeader, blockTimestamp uint64, forSpecificValidatorIndex *big.Int, verbose bool) (*eigenpodproofs.VerifyValidatorFieldsCallParams, error) {
func GenerateValidatorProofAtState(ctx context.Context, proofs *eigenpodproofs.EigenPodProofs, eigenpodAddress string, beaconState *spec.VersionedBeaconState, eth *ethclient.Client, chainId *big.Int, header *v1.BeaconBlockHeader, blockTimestamp uint64, forSpecificValidatorIndex *big.Int, verbose bool) (*eigenpodproofs.VerifyValidatorFieldsCallParams, error) {
allValidators, err := FindAllValidatorsForEigenpod(eigenpodAddress, beaconState)
if err != nil {
return nil, fmt.Errorf("failed to find validators: %w", err)
Expand All @@ -177,7 +177,7 @@ func GenerateValidatorProofAtState(proofs *eigenpodproofs.EigenPodProofs, eigenp
}
} else {
// default behavior -- load any validators that are inactive / need a credential proof
allValidatorsWithInfo, err := FetchMultipleOnchainValidatorInfo(eth, eigenpodAddress, allValidators)
allValidatorsWithInfo, err := FetchMultipleOnchainValidatorInfo(ctx, eth, eigenpodAddress, allValidators)
if err != nil {
return nil, fmt.Errorf("failed to load validator information: %s", err.Error())
}
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ require (
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
github.com/forta-network/go-multicall v0.0.0-20230701154355-9467c4ddaa83 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down
Loading
Loading