Skip to content

Commit

Permalink
Merge branch 'dev' into feat-improve-bls-logging
Browse files Browse the repository at this point in the history
  • Loading branch information
taturosati authored Aug 20, 2024
2 parents 7a4a809 + e5c3fb8 commit 88dab6d
Show file tree
Hide file tree
Showing 42 changed files with 3,646 additions and 1,215 deletions.
34 changes: 22 additions & 12 deletions chainio/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
## ChainIO

This module is used to facilitate reading/writing/subscribing to [eigenlayer core](./clients/elcontracts/) contracts and [avs registry](./clients/avsregistry/) contracts.

To make it easier to understand the different structs in this package, and their hierarchical relationship, we describe each of them below:
- geth's ethClient
- eigensdk [ethClient](./clients/eth/client.go)
- wraps geth's ethClient and adds convenience methods
- [eigenlayerContractBindings](../contracts/bindings/)
### Interacting with a json-rpc node

We have a basic [ethClient](./clients/eth/client.go) which simply wraps geth's ethClient and adds some convenience methods. The Client interface is also implemented by [instrumentedClient](./clients/eth/instrumented_client.go) which adds metrics to the ethClient to conform to the node spec's [rpc metrics](https://docs.eigenlayer.xyz/eigenlayer/avs-guides/spec/metrics/metrics-prom-spec#rpc-metrics) requirements.


### Building Transactions

In order to facilitate reading/writing/subscribing to [eigenlayer core](./clients/elcontracts/) contracts and [avs registry](./clients/avsregistry/) contracts, we use geth's abigen created bindings for low-level interactions, as well as our own high-level clients with higher utility functions:
- [Eigenlayer Contract Bindings](./clients/elcontracts/bindings.go)
- generated by abigen
- low level bindings to eigenlayer core contracts, which wrap our ethClient
- [elContractsClient](./clients/eth/client.go)
- wraps eigenlayerContractBindings and hides a little bit of the underlying complexity, which is not needed in 99% of cases.
- abigen also doesn't create an interface for the bindings it generates, whereas elContractsClient has a well defined interface which we use to generate mocks to help with testing.
- [ELChainReader](./clients/elcontracts/reader.go) / [ELChainWriter](./clients/elcontracts/writer.go) / [ELChainSubscriber](./clients/avsregistry/subscriber.go)
- wraps elContractsClient and adds convenience methods
- hides even more complexity than elContractsClient
- wraps bindings and adds convenience methods
- These structs should be the only ones used by AVS developers, apart from interacting with an ethClient directly to make direct json rpc calls such as waiting for a transaction receipt.

A similar hierarchy applies for the avs registry contracts.
There's a similar setup for the [avs registry](./clients/avsregistry/) contracts.

### Signing, Sending, and Managing Transactions

After building transactions, we need to sign them, send them to the network, and manage the nonce and gas price to ensure they are mined. This functionality is provided by:
- [txmgr](./txmgr/README.md)
- uses a wallet to sign and submit transactions, but then manages them by resubmitting with higher gas prices until they are mined.
- [wallet](./clients/wallet)
- uses a signerv2 to sign transactions, sends them to the network and can query for their receipts
- wallet abstraction is needed because "wallets", such as fireblocks, both sign and send transactions to the network (they don't simply return signed bytes so that we can send them ourselves)
- [signerv2](../signerv2/README.md)
- signs transactions
42 changes: 42 additions & 0 deletions chainio/clients/avsregistry/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,48 @@ import (
"github.com/Layr-Labs/eigensdk-go/logging"
)

// Build an AVS registry client with the given configuration,
// HTTP and WS clients, and logger, but without a private key.
//
// This is useful for read-only operations.
func BuildReadClients(
config Config,
client eth.HttpBackend,
wsClient eth.WsBackend,
logger logging.Logger,
) (*ChainReader, *ChainSubscriber, *ContractBindings, error) {
avsBindings, err := NewBindingsFromConfig(
config,
client,
logger,
)

if err != nil {
return nil, nil, nil, err
}

chainReader := NewChainReader(
avsBindings.RegistryCoordinatorAddr,
avsBindings.BlsApkRegistryAddr,
avsBindings.RegistryCoordinator,
avsBindings.OperatorStateRetriever,
avsBindings.StakeRegistry,
logger,
client,
)

chainSubscriber, err := NewSubscriberFromConfig(
config,
wsClient,
logger,
)
if err != nil {
return nil, nil, nil, err
}

return chainReader, chainSubscriber, avsBindings, nil
}

func BuildClients(
config Config,
client eth.HttpBackend,
Expand Down
18 changes: 12 additions & 6 deletions chainio/clients/avsregistry/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func (w *ChainWriter) RegisterOperatorInQuorumWithAVSRegistryCoordinator(
blsKeyPair *bls.KeyPair,
quorumNumbers types.QuorumNums,
socket string,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
operatorAddr := crypto.PubkeyToAddress(operatorEcdsaPrivateKey.PublicKey)
w.logger.Info(
Expand Down Expand Up @@ -271,7 +272,7 @@ func (w *ChainWriter) RegisterOperatorInQuorumWithAVSRegistryCoordinator(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send tx with err: " + err.Error())
}
Expand Down Expand Up @@ -302,6 +303,7 @@ func (w *ChainWriter) RegisterOperator(
blsKeyPair *bls.KeyPair,
quorumNumbers types.QuorumNums,
socket string,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
operatorAddr := crypto.PubkeyToAddress(operatorEcdsaPrivateKey.PublicKey)
w.logger.Info(
Expand Down Expand Up @@ -390,7 +392,7 @@ func (w *ChainWriter) RegisterOperator(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send tx with err: " + err.Error())
}
Expand All @@ -417,6 +419,7 @@ func (w *ChainWriter) UpdateStakesOfEntireOperatorSetForQuorums(
ctx context.Context,
operatorsPerQuorum [][]gethcommon.Address,
quorumNumbers types.QuorumNums,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
w.logger.Info("updating stakes for entire operator set", "quorumNumbers", quorumNumbers)
noSendTxOpts, err := w.txMgr.GetNoSendTxOpts()
Expand All @@ -431,7 +434,7 @@ func (w *ChainWriter) UpdateStakesOfEntireOperatorSetForQuorums(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send tx with err: " + err.Error())
}
Expand All @@ -449,6 +452,7 @@ func (w *ChainWriter) UpdateStakesOfEntireOperatorSetForQuorums(
func (w *ChainWriter) UpdateStakesOfOperatorSubsetForAllQuorums(
ctx context.Context,
operators []gethcommon.Address,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
w.logger.Info("updating stakes of operator subset for all quorums", "operators", operators)
noSendTxOpts, err := w.txMgr.GetNoSendTxOpts()
Expand All @@ -459,7 +463,7 @@ func (w *ChainWriter) UpdateStakesOfOperatorSubsetForAllQuorums(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send tx with err: " + err.Error())
}
Expand All @@ -477,6 +481,7 @@ func (w *ChainWriter) DeregisterOperator(
ctx context.Context,
quorumNumbers types.QuorumNums,
pubkey regcoord.BN254G1Point,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
w.logger.Info("deregistering operator with the AVS's registry coordinator")
noSendTxOpts, err := w.txMgr.GetNoSendTxOpts()
Expand All @@ -487,7 +492,7 @@ func (w *ChainWriter) DeregisterOperator(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send tx with err: " + err.Error())
}
Expand All @@ -502,6 +507,7 @@ func (w *ChainWriter) DeregisterOperator(
func (w *ChainWriter) UpdateSocket(
ctx context.Context,
socket types.Socket,
waitForReceipt bool,
) (*gethtypes.Receipt, error) {
noSendTxOpts, err := w.txMgr.GetNoSendTxOpts()
if err != nil {
Expand All @@ -511,7 +517,7 @@ func (w *ChainWriter) UpdateSocket(
if err != nil {
return nil, err
}
receipt, err := w.txMgr.Send(ctx, tx)
receipt, err := w.txMgr.Send(ctx, tx, waitForReceipt)
if err != nil {
return nil, errors.New("failed to send UpdateSocket tx with err: " + err.Error())
}
Expand Down
110 changes: 92 additions & 18 deletions chainio/clients/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,99 @@ type BuildAllConfig struct {
PromMetricsIpPortAddress string
}

// Clients is a struct that holds all the clients that are needed to interact with the AVS and EL contracts.
// TODO: this is confusing right now because clients are not instrumented clients, but
// we return metrics and prometheus reg, so user has to build instrumented clients at the call
// site if they need them. We should probably separate into two separate constructors, one
// for non-instrumented clients that doesn't return metrics/reg, and another instrumented-constructor
// that returns instrumented clients and the metrics/reg.
type Clients struct {
// ReadClients is a struct that holds only the read clients for interacting with the AVS and EL contracts.
type ReadClients struct {
AvsRegistryChainReader *avsregistry.ChainReader
AvsRegistryChainSubscriber *avsregistry.ChainSubscriber
AvsRegistryChainWriter *avsregistry.ChainWriter
ElChainReader *elcontracts.ChainReader
ElChainWriter *elcontracts.ChainWriter
EthHttpClient eth.HttpBackend
EthWsClient eth.WsBackend
Wallet wallet.Wallet
TxManager txmgr.TxManager
AvsRegistryContractBindings *avsregistry.ContractBindings
EigenlayerContractBindings *elcontracts.ContractBindings
Metrics *metrics.EigenMetrics // exposes main avs node spec metrics that need to be incremented by avs code and used to start the metrics server
PrometheusRegistry *prometheus.Registry // Used if avs teams need to register avs-specific metrics
PrometheusRegistry *prometheus.Registry
}

// Clients is a struct that holds all the clients that are needed to interact with the AVS and EL contracts.
type Clients struct {
ReadClients
Wallet wallet.Wallet
TxManager txmgr.TxManager
ElChainWriter *elcontracts.ChainWriter
AvsRegistryChainWriter *avsregistry.ChainWriter
}

// BuildReadClients creates all the read clients needed to interact with the AVS and EL contracts.
func BuildReadClients(
config BuildAllConfig,
logger logging.Logger,
) (*ReadClients, error) {
config.validate(logger)

// Create the metrics server
promReg := prometheus.NewRegistry()
eigenMetrics := metrics.NewEigenMetrics(config.AvsName, config.PromMetricsIpPortAddress, promReg, logger)

// creating two types of Eth clients: HTTP and WS
ethHttpClient, err := ethclient.Dial(config.EthHttpUrl)
if err != nil {
return nil, utils.WrapError("Failed to create Eth Http client", err)
}

ethWsClient, err := ethclient.Dial(config.EthWsUrl)
if err != nil {
return nil, utils.WrapError("Failed to create Eth WS client", err)
}

// creating AVS clients: Reader
avsRegistryChainReader, avsRegistryChainSubscriber, avsRegistryContractBindings, err := avsregistry.BuildReadClients(
avsregistry.Config{
RegistryCoordinatorAddress: gethcommon.HexToAddress(config.RegistryCoordinatorAddr),
OperatorStateRetrieverAddress: gethcommon.HexToAddress(config.OperatorStateRetrieverAddr),
},
ethHttpClient,
ethWsClient,
logger,
)
if err != nil {
return nil, utils.WrapError("Failed to create AVS Registry Reader and Writer", err)
}

// creating EL clients: Reader and EigenLayer Contract Bindings
elChainReader, elContractBindings, err := elcontracts.BuildReadClients(
elcontracts.Config{
DelegationManagerAddress: avsRegistryContractBindings.DelegationManagerAddr,
AvsDirectoryAddress: avsRegistryContractBindings.AvsDirectoryAddr,
},
ethHttpClient,
logger,
eigenMetrics,
)
if err != nil {
return nil, utils.WrapError("Failed to create EL Reader and Writer", err)
}

readClients := ReadClients{
ElChainReader: elChainReader,
AvsRegistryChainReader: avsRegistryChainReader,
AvsRegistryChainSubscriber: avsRegistryChainSubscriber,
EthHttpClient: ethHttpClient,
EthWsClient: ethWsClient,
EigenlayerContractBindings: elContractBindings,
AvsRegistryContractBindings: avsRegistryContractBindings,
Metrics: eigenMetrics,
PrometheusRegistry: promReg,
}
return &readClients, nil
}

// BuildAll creates all the clients needed to interact with the AVS and EL contracts. For both read and write
// operations.
// TODO: this is confusing right now because clients are not instrumented clients, but
// we return metrics and prometheus reg, so user has to build instrumented clients at the call
// site if they need them. We should probably separate into two separate constructors, one
// for non-instrumented clients that doesn't return metrics/reg, and another instrumented-constructor
// that returns instrumented clients and the metrics/reg.
func BuildAll(
config BuildAllConfig,
ecdsaPrivateKey *ecdsa.PrivateKey,
Expand Down Expand Up @@ -120,22 +191,25 @@ func BuildAll(
return nil, utils.WrapError("Failed to create EL Reader and Writer", err)
}

return &Clients{
readClients := ReadClients{
ElChainReader: elChainReader,
ElChainWriter: elChainWriter,
AvsRegistryChainReader: avsRegistryChainReader,
AvsRegistryChainSubscriber: avsRegistryChainSubscriber,
AvsRegistryChainWriter: avsRegistryChainWriter,
EthHttpClient: ethHttpClient,
EthWsClient: ethWsClient,
Wallet: pkWallet,
TxManager: txMgr,
EigenlayerContractBindings: elContractBindings,
AvsRegistryContractBindings: avsRegistryContractBindings,
Metrics: eigenMetrics,
PrometheusRegistry: promReg,
}, nil
}

return &Clients{
ReadClients: readClients,
ElChainWriter: elChainWriter,
AvsRegistryChainWriter: avsRegistryChainWriter,
Wallet: pkWallet,
TxManager: txMgr,
}, nil
}

// Very basic validation that makes sure all fields are nonempty
Expand Down
30 changes: 30 additions & 0 deletions chainio/clients/elcontracts/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ import (
"github.com/Layr-Labs/eigensdk-go/metrics"
)

// Returns a tuple of reader clients with the given:
// configuration, HTTP client, logger and metrics.
func BuildReadClients(
config Config,
client eth.HttpBackend,
logger logging.Logger,
eigenMetrics *metrics.EigenMetrics,
) (*ChainReader, *ContractBindings, error) {
elContractBindings, err := NewBindingsFromConfig(
config,
client,
logger,
)
if err != nil {
return nil, nil, err
}

elChainReader := NewChainReader(
elContractBindings.Slasher,
elContractBindings.DelegationManager,
elContractBindings.StrategyManager,
elContractBindings.AvsDirectory,
elContractBindings.RewardsCoordinator,
logger,
client,
)

return elChainReader, elContractBindings, nil
}

func BuildClients(
config Config,
client eth.HttpBackend,
Expand Down
20 changes: 20 additions & 0 deletions chainio/clients/elcontracts/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,23 @@ func (r *ChainReader) CurrRewardsCalculationEndTimestamp(opts *bind.CallOpts) (u

return r.rewardsCoordinator.CurrRewardsCalculationEndTimestamp(opts)
}

func (r *ChainReader) GetCurrentClaimableDistributionRoot(
opts *bind.CallOpts,
) (rewardscoordinator.IRewardsCoordinatorDistributionRoot, error) {
if r.rewardsCoordinator == nil {
return rewardscoordinator.IRewardsCoordinatorDistributionRoot{}, errors.New(
"RewardsCoordinator contract not provided",
)
}

return r.rewardsCoordinator.GetCurrentClaimableDistributionRoot(opts)
}

func (r *ChainReader) GetRootIndexFromHash(opts *bind.CallOpts, rootHash [32]byte) (uint32, error) {
if r.rewardsCoordinator == nil {
return 0, errors.New("RewardsCoordinator contract not provided")
}

return r.rewardsCoordinator.GetRootIndexFromHash(opts, rootHash)
}
Loading

0 comments on commit 88dab6d

Please sign in to comment.