Skip to content

Commit

Permalink
Merge pull request #151 from Layr-Labs/jb/multicall3
Browse files Browse the repository at this point in the history
Support `multicall3` in status endpoint
  • Loading branch information
jbrower95 authored Sep 6, 2024
2 parents 60175b9 + 679c771 commit c0f3579
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 27 deletions.
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

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

0 comments on commit c0f3579

Please sign in to comment.