diff --git a/Makefile b/Makefile index 970162d7..aa2b821b 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ ############################# HELP MESSAGE ############################# # Make sure the help command stays first, so that it's printed by default when `make` is called without arguments -.PHONY: help bindings mocks tests tests-cover fmt format-lines +.PHONY: help bindings mocks tests tests-cover fmt format-lines lint GO_LINES_IGNORED_DIRS=contracts GO_PACKAGES=./chainio/... ./crypto/... ./logging/... \ - ./types/... ./utils/... ./signer/... ./cmd/... + ./types/... ./utils/... ./signer/... ./cmd/... \ + ./signerv2/... GO_FOLDERS=$(shell echo ${GO_PACKAGES} | sed -e "s/\.\///g" | sed -e "s/\/\.\.\.//g") help: @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @@ -43,4 +44,8 @@ fmt: ## formats all go files format-lines: ## formats all go files with golines go install github.com/segmentio/golines@latest - golines -w -m 120 --ignore-generated --shorten-comments --ignored-dirs=${GO_LINES_IGNORED_DIRS} ${GO_FOLDERS} \ No newline at end of file + golines -w -m 120 --ignore-generated --shorten-comments --ignored-dirs=${GO_LINES_IGNORED_DIRS} ${GO_FOLDERS} + +lint: ## runs all linters + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + golangci-lint run ./... \ No newline at end of file diff --git a/chainio/clients/avsregistry/writer.go b/chainio/clients/avsregistry/writer.go index beb7b4c5..c5a65d65 100644 --- a/chainio/clients/avsregistry/writer.go +++ b/chainio/clients/avsregistry/writer.go @@ -2,10 +2,11 @@ package avsregistry import ( "context" + "errors" "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" + "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" "github.com/Layr-Labs/eigensdk-go/logging" - "github.com/Layr-Labs/eigensdk-go/signer" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -40,9 +41,9 @@ type AvsRegistryChainWriter struct { blsOperatorStateRetriever *blsoperatorstateretriever.ContractBLSOperatorStateRetriever stakeRegistry *stakeregistry.ContractStakeRegistry blsPubkeyRegistry *blspubkeyregistry.ContractBLSPubkeyRegistry - signer signer.Signer logger logging.Logger ethClient eth.EthClient + txMgr txmgr.TxManager } var _ AvsRegistryWriter = (*AvsRegistryChainWriter)(nil) @@ -53,17 +54,17 @@ func NewAvsRegistryWriter( stakeRegistry *stakeregistry.ContractStakeRegistry, blsPubkeyRegistry *blspubkeyregistry.ContractBLSPubkeyRegistry, logger logging.Logger, - signer signer.Signer, ethClient eth.EthClient, + txMgr txmgr.TxManager, ) (*AvsRegistryChainWriter, error) { return &AvsRegistryChainWriter{ registryCoordinator: registryCoordinator, blsOperatorStateRetriever: blsOperatorStateRetriever, stakeRegistry: stakeRegistry, blsPubkeyRegistry: blsPubkeyRegistry, - signer: signer, logger: logger, ethClient: ethClient, + txMgr: txMgr, }, nil } @@ -74,15 +75,21 @@ func (w *AvsRegistryChainWriter) RegisterOperatorWithAVSRegistryCoordinator( socket string, ) (*types.Receipt, error) { w.logger.Info("registering operator with the AVS's registry coordinator") - txOpts := w.signer.GetTxOpts() + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } // TODO: this call will fail if max number of operators are already registered // in that case, need to call churner to kick out another operator. See eigenDA's node/operator.go implementation - tx, err := w.registryCoordinator.RegisterOperatorWithCoordinator1(txOpts, quorumNumbers, pubkey, socket) + tx, err := w.registryCoordinator.RegisterOperatorWithCoordinator1(noSendTxOpts, quorumNumbers, pubkey, socket) if err != nil { return nil, err } + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("tx hash: %s", tx.Hash().String()) - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) w.logger.Info("registered operator with the AVS's registry coordinator") return receipt, nil } @@ -92,13 +99,19 @@ func (w *AvsRegistryChainWriter) UpdateStakes( operators []gethcommon.Address, ) (*types.Receipt, error) { w.logger.Info("updating stakes") - txOpts := w.signer.GetTxOpts() - tx, err := w.stakeRegistry.UpdateStakes(txOpts, operators) + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } + tx, err := w.stakeRegistry.UpdateStakes(noSendTxOpts, operators) if err != nil { return nil, err } + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("tx hash: %s", tx.Hash().String()) - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) w.logger.Info("updated stakes") return receipt, nil @@ -110,13 +123,19 @@ func (w *AvsRegistryChainWriter) DeregisterOperator( pubkey blsregistrycoordinator.BN254G1Point, ) (*types.Receipt, error) { w.logger.Info("deregistering operator with the AVS's registry coordinator") - txOpts := w.signer.GetTxOpts() - tx, err := w.registryCoordinator.DeregisterOperatorWithCoordinator(txOpts, quorumNumbers, pubkey) + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() if err != nil { return nil, err } + tx, err := w.registryCoordinator.DeregisterOperatorWithCoordinator(noSendTxOpts, quorumNumbers, pubkey) + if err != nil { + return nil, err + } + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("tx hash: %s", tx.Hash().String()) - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) w.logger.Info("deregistered operator with the AVS's registry coordinator") return receipt, nil } diff --git a/chainio/clients/builder.go b/chainio/clients/builder.go index b141175e..47ca9c3b 100644 --- a/chainio/clients/builder.go +++ b/chainio/clients/builder.go @@ -1,14 +1,15 @@ package clients import ( - avsregistry "github.com/Layr-Labs/eigensdk-go/chainio/clients/avsregistry" - elcontracts "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/avsregistry" + "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" + "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" chainioutils "github.com/Layr-Labs/eigensdk-go/chainio/utils" blspubkeycompendium "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSPublicKeyCompendium" - logging "github.com/Layr-Labs/eigensdk-go/logging" + "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/metrics" - "github.com/Layr-Labs/eigensdk-go/signer" + "github.com/Layr-Labs/eigensdk-go/signerv2" "github.com/ethereum/go-ethereum/accounts/abi/bind" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/prometheus/client_golang/prometheus" @@ -40,7 +41,7 @@ type Clients struct { PrometheusRegistry *prometheus.Registry // Used if avs teams need to register avs-specific metrics } -func BuildAll(config BuildAllConfig, signer signer.Signer, logger logging.Logger) (*Clients, error) { +func BuildAll(config BuildAllConfig, signer signerv2.SignerFn, logger logging.Logger) (*Clients, error) { config.validate(logger) // Create the metrics server @@ -60,11 +61,12 @@ func BuildAll(config BuildAllConfig, signer signer.Signer, logger logging.Logger return nil, err } + txMgr := txmgr.NewSimpleTxManager(ethHttpClient, logger, signer, gethcommon.Address{}) // creating EL clients: Reader, Writer and Subscriber elChainReader, elChainWriter, elChainSubscriber, err := config.buildElClients( ethHttpClient, ethWsClient, - signer, + txMgr, logger, eigenMetrics, ) @@ -76,7 +78,7 @@ func BuildAll(config BuildAllConfig, signer signer.Signer, logger logging.Logger // creating AVS clients: Reader and Writer avsRegistryChainReader, avsRegistryChainWriter, err := config.buildAvsClients( ethHttpClient, - signer, + txMgr, logger, ) if err != nil { @@ -101,7 +103,7 @@ func BuildAll(config BuildAllConfig, signer signer.Signer, logger logging.Logger func (config *BuildAllConfig) buildElClients( ethHttpClient eth.EthClient, ethWsClient eth.EthClient, - signer signer.Signer, + txMgr txmgr.TxManager, logger logging.Logger, eigenMetrics *metrics.EigenMetrics, ) (elcontracts.ELReader, elcontracts.ELWriter, elcontracts.ELSubscriber, error) { @@ -145,7 +147,10 @@ func (config *BuildAllConfig) buildElClients( ) // get the Subscriber for the EL contracts - contractBlsPubkeyCompendiumWs, err := blspubkeycompendium.NewContractBLSPublicKeyCompendium(elContractBindings.BlspubkeyCompendiumAddr, ethWsClient) + contractBlsPubkeyCompendiumWs, err := blspubkeycompendium.NewContractBLSPublicKeyCompendium( + elContractBindings.BlspubkeyCompendiumAddr, + ethWsClient, + ) if err != nil { logger.Fatal("Failed to fetch BLSPublicKeyCompendium contract", "err", err) } @@ -167,9 +172,9 @@ func (config *BuildAllConfig) buildElClients( elContractBindings.BlspubkeyCompendiumAddr, elChainReader, ethHttpClient, - signer, logger, eigenMetrics, + txMgr, ) if err != nil { logger.Error("Failed to create ELChainWriter", "err", err) @@ -181,7 +186,7 @@ func (config *BuildAllConfig) buildElClients( func (config *BuildAllConfig) buildAvsClients( ethHttpClient eth.EthClient, - signer signer.Signer, + txMgr txmgr.TxManager, logger logging.Logger, ) (avsregistry.AvsRegistryReader, avsregistry.AvsRegistryWriter, error) { @@ -215,8 +220,8 @@ func (config *BuildAllConfig) buildAvsClients( avsRegistryContractBindings.StakeRegistry, avsRegistryContractBindings.BlsPubkeyRegistry, logger, - signer, ethHttpClient, + txMgr, ) if err != nil { logger.Error("Failed to create AVSRegistryChainWriter", "err", err) diff --git a/chainio/clients/elcontracts/reader.go b/chainio/clients/elcontracts/reader.go index f3fee20b..4aec4b79 100644 --- a/chainio/clients/elcontracts/reader.go +++ b/chainio/clients/elcontracts/reader.go @@ -104,7 +104,12 @@ func BuildELChainReader( ethClient eth.EthClient, logger logging.Logger, ) (*ELChainReader, error) { - elContractBindings, err := chainioutils.NewEigenlayerContractBindings(slasherAddr, blsPubKeyCompendiumAddr, ethClient, logger) + elContractBindings, err := chainioutils.NewEigenlayerContractBindings( + slasherAddr, + blsPubKeyCompendiumAddr, + ethClient, + logger, + ) if err != nil { return nil, err } diff --git a/chainio/clients/elcontracts/writer.go b/chainio/clients/elcontracts/writer.go index 8d80d757..7864ce83 100644 --- a/chainio/clients/elcontracts/writer.go +++ b/chainio/clients/elcontracts/writer.go @@ -2,8 +2,10 @@ package elcontracts import ( "context" + "errors" "math/big" + "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" "github.com/ethereum/go-ethereum/accounts/abi/bind" gethcommon "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" @@ -14,7 +16,6 @@ import ( "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/metrics" - "github.com/Layr-Labs/eigensdk-go/signer" "github.com/Layr-Labs/eigensdk-go/types" blspubkeycompendium "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSPublicKeyCompendium" @@ -53,8 +54,8 @@ type ELChainWriter struct { blsPubkeyCompendiumAddr gethcommon.Address elChainReader ELReader ethClient eth.EthClient - signer signer.Signer logger logging.Logger + txMgr txmgr.TxManager } var _ ELWriter = (*ELChainWriter)(nil) @@ -68,9 +69,9 @@ func NewELChainWriter( blsPubkeyCompendiumAddr gethcommon.Address, elChainReader ELReader, ethClient eth.EthClient, - signer signer.Signer, logger logging.Logger, eigenMetrics metrics.Metrics, + txMgr txmgr.TxManager, ) *ELChainWriter { return &ELChainWriter{ slasher: slasher, @@ -80,9 +81,9 @@ func NewELChainWriter( blsPubkeyCompendium: blsPubkeyCompendium, blsPubkeyCompendiumAddr: blsPubkeyCompendiumAddr, elChainReader: elChainReader, - signer: signer, logger: logger, ethClient: ethClient, + txMgr: txMgr, } } @@ -90,11 +91,16 @@ func BuildELChainWriter( slasherAddr gethcommon.Address, blsPubKeyCompendiumAddr gethcommon.Address, ethClient eth.EthClient, - signer signer.Signer, logger logging.Logger, eigenMetrics metrics.Metrics, + txMgr txmgr.TxManager, ) (*ELChainWriter, error) { - elContractBindings, err := chainioutils.NewEigenlayerContractBindings(slasherAddr, blsPubKeyCompendiumAddr, ethClient, logger) + elContractBindings, err := chainioutils.NewEigenlayerContractBindings( + slasherAddr, + blsPubKeyCompendiumAddr, + ethClient, + logger, + ) if err != nil { return nil, err } @@ -116,9 +122,9 @@ func BuildELChainWriter( blsPubKeyCompendiumAddr, elChainReader, ethClient, - signer, logger, eigenMetrics, + txMgr, ), nil } @@ -131,15 +137,21 @@ func (w *ELChainWriter) RegisterAsOperator(ctx context.Context, operator types.O EarningsReceiver: gethcommon.HexToAddress(operator.EarningsReceiverAddress), StakerOptOutWindowBlocks: operator.StakerOptOutWindowBlocks, } - txOpts := w.signer.GetTxOpts() - tx, err := w.delegationManager.RegisterAsOperator(txOpts, opDetails, operator.MetadataUrl) + + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } + tx, err := w.delegationManager.RegisterAsOperator(noSendTxOpts, opDetails, operator.MetadataUrl) if err != nil { return nil, err } + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("tx hash: %s", tx.Hash().String()) - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) - w.logger.Infof("registered operator %s with EigenLayer", operator.Address) return receipt, nil } @@ -147,7 +159,6 @@ func (w *ELChainWriter) UpdateOperatorDetails( ctx context.Context, operator types.Operator, ) (*gethtypes.Receipt, error) { - txOpts := w.signer.GetTxOpts() w.logger.Infof("updating operator details of operator %s to EigenLayer", operator.Address) opDetails := delegationmanager.IDelegationManagerOperatorDetails{ @@ -156,20 +167,31 @@ func (w *ELChainWriter) UpdateOperatorDetails( StakerOptOutWindowBlocks: operator.StakerOptOutWindowBlocks, } - tx, err := w.delegationManager.ModifyOperatorDetails(txOpts, opDetails) + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() if err != nil { return nil, err } - w.logger.Infof("tx hash: %s", tx.Hash().String()) - w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) + tx, err := w.delegationManager.ModifyOperatorDetails(noSendTxOpts, opDetails) + if err != nil { + return nil, err + } + _, err = w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } + w.logger.Infof("tx hash: %s", tx.Hash().String()) w.logger.Infof("updated operator metadata URI for operator %s to EigenLayer", operator.Address) - tx, err = w.delegationManager.UpdateOperatorMetadataURI(txOpts, operator.MetadataUrl) + + tx, err = w.delegationManager.UpdateOperatorMetadataURI(noSendTxOpts, operator.MetadataUrl) if err != nil { return nil, err } + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("tx hash: %s", tx.Hash().String()) - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) w.logger.Infof("updated operator details of operator %s to EigenLayer", operator.Address) return receipt, nil @@ -181,7 +203,10 @@ func (w *ELChainWriter) DepositERC20IntoStrategy( amount *big.Int, ) (*gethtypes.Receipt, error) { w.logger.Infof("depositing %s tokens into strategy %s", amount.String(), strategyAddr) - txOpts := w.signer.GetTxOpts() + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } _, underlyingTokenContract, underlyingTokenAddr, err := w.elChainReader.GetStrategyAndUnderlyingERC20Token( &bind.CallOpts{Context: ctx}, strategyAddr, @@ -190,40 +215,50 @@ func (w *ELChainWriter) DepositERC20IntoStrategy( return nil, err } - tx, err := underlyingTokenContract.Approve(txOpts, w.strategyManagerAddr, amount) + tx, err := underlyingTokenContract.Approve(noSendTxOpts, w.strategyManagerAddr, amount) if err != nil { return nil, err } - // not sure if this is necessary or if nonce management will be smart enough to queue them one after the other, - // but playing it safe by waiting for approve tx to be mined before sending deposit tx - w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) + _, err = w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } - tx, err = w.strategyManager.DepositIntoStrategy(txOpts, strategyAddr, underlyingTokenAddr, amount) + tx, err = w.strategyManager.DepositIntoStrategy(noSendTxOpts, strategyAddr, underlyingTokenAddr, amount) if err != nil { return nil, err } - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("deposited %s into strategy %s", amount.String(), strategyAddr) return receipt, nil } -// operator opting into slashing is the w.signer wallet +// OptOperatorIntoSlashing operator opting into slashing is the w.signer wallet // this is meant to be called by the operator CLI func (w *ELChainWriter) OptOperatorIntoSlashing( ctx context.Context, avsServiceManagerAddr gethcommon.Address, ) (*gethtypes.Receipt, error) { - txOpts := w.signer.GetTxOpts() - tx, err := w.slasher.OptIntoSlashing(txOpts, avsServiceManagerAddr) + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } + tx, err := w.slasher.OptIntoSlashing(noSendTxOpts, avsServiceManagerAddr) if err != nil { return nil, err } - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof( "Operator %s opted into slashing by service manager contract %s \n", - txOpts.From, + noSendTxOpts.From, avsServiceManagerAddr, ) return receipt, nil @@ -235,7 +270,10 @@ func (w *ELChainWriter) RegisterBLSPublicKey( operator types.Operator, ) (*gethtypes.Receipt, error) { w.logger.Infof("Registering BLS Public key to eigenlayer for operator %s", operator.Address) - txOpts := w.signer.GetTxOpts() + noSendTxOpts, err := w.txMgr.GetNoSendTxOpts() + if err != nil { + return nil, err + } chainID, err := w.ethClient.ChainID(ctx) if err != nil { return nil, err @@ -245,11 +283,11 @@ func (w *ELChainWriter) RegisterBLSPublicKey( w.blsPubkeyCompendiumAddr, chainID, ) - signedMsgHashBN254 := blspubkeycompendium.BN254G1Point(utils.ConvertToBN254G1Point(signedMsgHash)) - G1pubkeyBN254 := blspubkeycompendium.BN254G1Point(utils.ConvertToBN254G1Point(blsKeyPair.GetPubKeyG1())) - G2pubkeyBN254 := blspubkeycompendium.BN254G2Point(utils.ConvertToBN254G2Point(blsKeyPair.GetPubKeyG2())) + signedMsgHashBN254 := utils.ConvertToBN254G1Point(signedMsgHash) + G1pubkeyBN254 := utils.ConvertToBN254G1Point(blsKeyPair.GetPubKeyG1()) + G2pubkeyBN254 := utils.ConvertToBN254G2Point(blsKeyPair.GetPubKeyG2()) tx, err := w.blsPubkeyCompendium.RegisterBLSPublicKey( - txOpts, + noSendTxOpts, signedMsgHashBN254, G1pubkeyBN254, G2pubkeyBN254, @@ -257,7 +295,10 @@ func (w *ELChainWriter) RegisterBLSPublicKey( if err != nil { return nil, err } - receipt := w.ethClient.WaitForTransactionReceipt(ctx, tx.Hash()) + receipt, err := w.txMgr.Send(ctx, tx) + if err != nil { + return nil, errors.New("failed to send tx with err: " + err.Error()) + } w.logger.Infof("Operator %s has registered BLS public key to EigenLayer \n", operator.Address) return receipt, nil diff --git a/chainio/txmgr/README.md b/chainio/txmgr/README.md new file mode 100644 index 00000000..186d3647 --- /dev/null +++ b/chainio/txmgr/README.md @@ -0,0 +1,12 @@ +## Transaction Manager +Transaction Manager is responsible for +* Building transactions +* Estimating fees and adding buffer +* Signing transactions +* Sending transactions to the network + + +### Simple Transaction Manager +Here's the flow of the simple transaction manager which is used to send smart contract +transactions to the network. +![Simple Transaction Manager](./simple-tx-manager-flow.png) diff --git a/chainio/txmgr/simple-tx-manager-flow.png b/chainio/txmgr/simple-tx-manager-flow.png new file mode 100644 index 00000000..4b07b30b Binary files /dev/null and b/chainio/txmgr/simple-tx-manager-flow.png differ diff --git a/chainio/txmgr/txmgr.go b/chainio/txmgr/txmgr.go new file mode 100644 index 00000000..cbb3ea43 --- /dev/null +++ b/chainio/txmgr/txmgr.go @@ -0,0 +1,234 @@ +package txmgr + +import ( + "context" + "errors" + "fmt" + "math/big" + "time" + + "github.com/Layr-Labs/eigensdk-go/logging" + "github.com/Layr-Labs/eigensdk-go/signerv2" + "github.com/ethereum/go-ethereum" + "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" + "github.com/ethereum/go-ethereum/log" +) + +var ( + FallbackGasTipCap = big.NewInt(15_000_000_000) +) + +// We are taking inspiration from the optimism TxManager interface +// https://github.com/ethereum-optimism/optimism/blob/develop/op-service/txmgr/txmgr.go + +type TxManager interface { + // Send is used to send a transaction + // It takes an unsigned transaction and then signs it before sending + // It also takes care of nonce management and gas estimation + Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) + + // GetNoSendTxOpts This generates a noSend TransactOpts so that we can use + // this to generate the transaction without actually sending it + GetNoSendTxOpts() (*bind.TransactOpts, error) +} + +type EthBackend interface { + bind.ContractBackend + + // TransactionReceipt queries the backend for a receipt associated with + // txHash. If lookup does not fail, but the transaction is not found, + // nil should be returned for both values. + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) + + ChainID(ctx context.Context) (*big.Int, error) +} + +type SimpleTxManager struct { + backend EthBackend + signerFn signerv2.SignerFn + log logging.Logger + sender common.Address + contracts map[common.Address]*bind.BoundContract +} + +var _ TxManager = (*SimpleTxManager)(nil) + +// NewSimpleTxManager creates a new simpleTxManager which can be used +// to send a transaction to smart contracts on the Ethereum node +func NewSimpleTxManager( + backend EthBackend, + log logging.Logger, + signerFn signerv2.SignerFn, + sender common.Address, +) *SimpleTxManager { + return &SimpleTxManager{ + backend: backend, + log: log, + signerFn: signerFn, + sender: sender, + contracts: map[common.Address]*bind.BoundContract{}, + } +} + +// Send is used to send a transaction to the Ethereum node. It takes an unsigned/signed transaction +// and then sends it to the Ethereum node. +// It also takes care of gas estimation and adds a buffer to the gas limit +// If you pass in a signed transaction it will ignore the signature +// and resign the transaction after adding the nonce and gas limit. +// To check out the whole flow on how this works, check out the README.md in this folder +func (m *SimpleTxManager) Send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { + + // Estimate gas and nonce + m.log.Debug("Estimating gas and nonce", "tx", tx.Hash().Hex()) + tx, err := m.estimateGasAndNonce(ctx, tx) + if err != nil { + return nil, err + } + + m.log.Debug("Getting signer for tx", "tx", tx.Hash().Hex()) + signer, err := m.signerFn(ctx, m.sender) + if err != nil { + return nil, err + } + + m.log.Debug("Sending transaction", "tx", tx.Hash().Hex()) + opts := &bind.TransactOpts{ + From: m.sender, + Nonce: new(big.Int).SetUint64(tx.Nonce()), + Signer: signer, + Value: tx.Value(), + GasFeeCap: tx.GasFeeCap(), + GasTipCap: tx.GasTipCap(), + GasLimit: tx.Gas(), + Context: ctx, + } + + contract := m.contracts[*tx.To()] + // if the contract has not been cached + if contract == nil { + // create a dummy bound contract tied to the `to` address of the transaction + contract = bind.NewBoundContract(*tx.To(), abi.ABI{}, m.backend, m.backend, m.backend) + // cache the contract for later use + m.contracts[*tx.To()] = contract + } + + tx, err = contract.RawTransact(opts, tx.Data()) + if err != nil { + return nil, fmt.Errorf("send: failed to send txn: %w", err) + } + if err != nil { + return nil, err + } + + receipt := m.waitForTx(ctx, tx) + + return receipt, nil +} + +// GetNoSendTxOpts This generates a noSend TransactOpts so that we can use +// this to generate the transaction without actually sending it +func (m *SimpleTxManager) GetNoSendTxOpts() (*bind.TransactOpts, error) { + signer, err := m.signerFn(context.Background(), m.sender) + if err != nil { + return nil, err + } + noSendTxOpts := &bind.TransactOpts{ + From: m.sender, + Signer: signer, + NoSend: true, + } + return noSendTxOpts, nil +} + +// estimateGasAndNonce we are explicitly implementing this because +// * We want to support legacy transactions (i.e. not dynamic fee) +// * We want to support gas management, i.e. add buffer to gas limit +func (m *SimpleTxManager) estimateGasAndNonce(ctx context.Context, tx *types.Transaction) (*types.Transaction, error) { + gasTipCap, err := m.backend.SuggestGasTipCap(ctx) + if err != nil { + // If the transaction failed because the backend does not support + // eth_maxPriorityFeePerGas, fallback to using the default constant. + m.log.Info("eth_maxPriorityFeePerGas is unsupported by current backend, using fallback gasTipCap") + gasTipCap = FallbackGasTipCap + } + + header, err := m.backend.HeaderByNumber(ctx, nil) + if err != nil { + return nil, err + } + + gasFeeCap := new(big.Int).Add(header.BaseFee, gasTipCap) + + gasLimit, err := m.backend.EstimateGas(ctx, ethereum.CallMsg{ + From: m.sender, + To: tx.To(), + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Value: tx.Value(), + Data: tx.Data(), + }) + if err != nil { + return nil, err + } + + rawTx := &types.DynamicFeeTx{ + ChainID: tx.ChainId(), + To: tx.To(), + GasTipCap: gasTipCap, + GasFeeCap: gasFeeCap, + Data: tx.Data(), + Value: tx.Value(), + Gas: gasLimit, // TODO(add buffer) + Nonce: tx.Nonce(), // We are not doing any nonce management for now but we probably should later for more robustness + } + + return types.NewTx(rawTx), nil +} + +// waitForTx calls waitMined, and then return the receipt +func (m *SimpleTxManager) waitForTx(ctx context.Context, tx *types.Transaction) *types.Receipt { + // Poll for the transaction to be ready & then send the result to receiptChan + receipt, err := m.waitMined(ctx, tx) + if err != nil { + log.Info("Transaction receipt not found", "err", err) + return nil + } + return receipt +} + +// waitMined waits for the transaction to be mined or for the context to be cancelled. +func (m *SimpleTxManager) waitMined(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { + txHash := tx.Hash() + queryTicker := time.NewTicker(2 * time.Second) + defer queryTicker.Stop() + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-queryTicker.C: + if receipt := m.queryReceipt(ctx, txHash); receipt != nil { + return receipt, nil + } + } + } +} + +// queryReceipt queries for the receipt and returns the receipt +func (m *SimpleTxManager) queryReceipt(ctx context.Context, txHash common.Hash) *types.Receipt { + receipt, err := m.backend.TransactionReceipt(ctx, txHash) + if errors.Is(err, ethereum.NotFound) { + m.log.Info("Transaction not yet mined", "hash", txHash) + return nil + } else if err != nil { + m.log.Info("Receipt retrieval failed", "hash", txHash, "err", err) + return nil + } else if receipt == nil { + m.log.Warn("Receipt and error are both nil", "hash", txHash) + return nil + } + + return receipt +} diff --git a/cmd/egnaddrs/main.go b/cmd/egnaddrs/main.go index c99efaf4..bb99847b 100644 --- a/cmd/egnaddrs/main.go +++ b/cmd/egnaddrs/main.go @@ -114,10 +114,12 @@ func getRegistryCoordinatorAddr(c *cli.Context, client *ethclient.Client) (commo serviceManagerAddrString := c.String(ServiceManagerAddrFlag.Name) if serviceManagerAddrString != "" { serviceManagerAddr := common.HexToAddress(serviceManagerAddrString) - // we use the IBLSSignatureChecker interface because the IServiceManager interface doesn't have a getter for the registry coordinator + // we use the IBLSSignatureChecker interface because the IServiceManager interface doesn't have a getter for the + // registry coordinator // because we don't want to restrict teams to use our registry contracts. - // However, egnaddrs is targetted at teams using our registry contracts, so we assume that they are using our registries - // and that their service manager inherits the IBLSSignatureChecker interface (to check signatures against the BLSPubkeyRegistry). + // However, egnaddrs is targetted at teams using our registry contracts, so we assume that they are using our + // registries and that their service manager inherits the IBLSSignatureChecker interface (to check signatures + // against the BLSPubkeyRegistry). serviceManagerContract, err := iblssigchecker.NewContractIBLSSignatureChecker(serviceManagerAddr, client) if err != nil { return common.Address{}, err @@ -132,7 +134,10 @@ func getRegistryCoordinatorAddr(c *cli.Context, client *ethclient.Client) (commo } func getAvsContractAddrs(client *ethclient.Client, registryCoordinatorAddr common.Address) (map[string]string, error) { - blsRegistryCoordinatorWithIndicesC, err := blsregistrycoordinator.NewContractBLSRegistryCoordinatorWithIndices(registryCoordinatorAddr, client) + blsRegistryCoordinatorWithIndicesC, err := blsregistrycoordinator.NewContractBLSRegistryCoordinatorWithIndices( + registryCoordinatorAddr, + client, + ) if err != nil { return nil, err } @@ -178,8 +183,14 @@ func getAvsContractAddrs(client *ethclient.Client, registryCoordinatorAddr commo return addrsDict, nil } -func getEigenlayerContractAddrs(client *ethclient.Client, registryCoordinatorAddr common.Address) (map[string]string, error) { - blsRegistryCoordinatorWithIndicesC, err := blsregistrycoordinator.NewContractBLSRegistryCoordinatorWithIndices(registryCoordinatorAddr, client) +func getEigenlayerContractAddrs( + client *ethclient.Client, + registryCoordinatorAddr common.Address, +) (map[string]string, error) { + blsRegistryCoordinatorWithIndicesC, err := blsregistrycoordinator.NewContractBLSRegistryCoordinatorWithIndices( + registryCoordinatorAddr, + client, + ) if err != nil { return nil, err } diff --git a/cmd/egnaddrs/main_test.go b/cmd/egnaddrs/main_test.go index bd4fe75f..0c4abcf8 100644 --- a/cmd/egnaddrs/main_test.go +++ b/cmd/egnaddrs/main_test.go @@ -59,7 +59,11 @@ func startAnvilTestContainer() testcontainers.Container { Mounts: testcontainers.ContainerMounts{ testcontainers.ContainerMount{ Source: testcontainers.GenericBindMountSource{ - HostPath: filepath.Join(integrationDir, "test_data", "avs-and-eigenlayer-deployed-anvil-state.json"), + HostPath: filepath.Join( + integrationDir, + "test_data", + "avs-and-eigenlayer-deployed-anvil-state.json", + ), }, Target: "/root/.anvil/state.json", }, diff --git a/crypto/ecdsa/utils.go b/crypto/ecdsa/utils.go index 8e4faa47..c769d628 100644 --- a/crypto/ecdsa/utils.go +++ b/crypto/ecdsa/utils.go @@ -3,10 +3,13 @@ package ecdsa import ( "bufio" "crypto/ecdsa" + "encoding/json" "fmt" "os" "path/filepath" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/crypto" "github.com/google/uuid" @@ -88,3 +91,25 @@ func ReadKey(keyStoreFile string, password string) (*ecdsa.PrivateKey, error) { return sk.PrivateKey, nil } + +// GetAddressFromKeyStoreFile We are using Web3 format defined by +// https://ethereum.org/en/developers/docs/data-structures-and-encoding/web3-secret-storage/ +func GetAddressFromKeyStoreFile(keyStoreFile string) (gethcommon.Address, error) { + keyJson, err := os.ReadFile(keyStoreFile) + if err != nil { + return gethcommon.Address{}, err + } + + // The reason we have map[string]interface{} is because `address` is string but the `crypto` field is an object + // we don't care about the object in this method, but we still need to unmarshal it + m := make(map[string]interface{}) + if err := json.Unmarshal(keyJson, &m); err != nil { + return gethcommon.Address{}, err + } + + if address, ok := m["address"].(string); !ok { + return gethcommon.Address{}, fmt.Errorf("address not found in key file") + } else { + return gethcommon.HexToAddress(address), nil + } +} diff --git a/metrics/eigenmetrics_example_test.go b/metrics/eigenmetrics_example_test.go index 5a088af0..445cb957 100644 --- a/metrics/eigenmetrics_example_test.go +++ b/metrics/eigenmetrics_example_test.go @@ -14,7 +14,7 @@ import ( "github.com/Layr-Labs/eigensdk-go/metrics" "github.com/Layr-Labs/eigensdk-go/metrics/collectors/economic" rpccalls "github.com/Layr-Labs/eigensdk-go/metrics/collectors/rpc_calls" - "github.com/Layr-Labs/eigensdk-go/signer" + "github.com/Layr-Labs/eigensdk-go/signerv2" "github.com/Layr-Labs/eigensdk-go/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -37,7 +37,8 @@ func ExampleEigenMetrics() { if err != nil { panic(err) } - privateKeySigner, err := signer.NewPrivateKeySigner(ecdsaPrivateKey, big.NewInt(1)) + + signerV2, _, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, big.NewInt(1)) if err != nil { panic(err) } @@ -50,7 +51,7 @@ func ExampleEigenMetrics() { AvsName: "exampleAvs", PromMetricsIpPortAddress: ":9090", } - clients, err := clients.BuildAll(chainioConfig, privateKeySigner, logger) + clients, err := clients.BuildAll(chainioConfig, signerV2, logger) if err != nil { panic(err) } diff --git a/signerv2/config.go b/signerv2/config.go new file mode 100644 index 00000000..7600b880 --- /dev/null +++ b/signerv2/config.go @@ -0,0 +1,26 @@ +package signerv2 + +import "crypto/ecdsa" + +type Config struct { + PrivateKey *ecdsa.PrivateKey + KeystorePath string + Password string + Endpoint string + Address string +} + +func (c Config) IsPrivateKeySigner() bool { + return c.PrivateKey != nil +} + +func (c Config) IsLocalKeystoreSigner() bool { + return c.KeystorePath != "" +} + +func (c Config) IsRemoteSigner() bool { + if c.Endpoint == "" || c.Address == "" { + return false + } + return true +} diff --git a/signerv2/signer.go b/signerv2/signer.go new file mode 100644 index 00000000..8b091bad --- /dev/null +++ b/signerv2/signer.go @@ -0,0 +1,76 @@ +package signerv2 + +import ( + "context" + "crypto/ecdsa" + "errors" + "math/big" + + sdkEcdsa "github.com/Layr-Labs/eigensdk-go/crypto/ecdsa" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +type SignerFn func(ctx context.Context, address common.Address) (bind.SignerFn, error) + +func PrivateKeySignerFn(privateKey *ecdsa.PrivateKey, chainID *big.Int) (bind.SignerFn, error) { + from := crypto.PubkeyToAddress(privateKey.PublicKey) + signer := types.LatestSignerForChainID(chainID) + + return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != from { + return nil, bind.ErrNotAuthorized + } + signature, err := crypto.Sign(signer.Hash(tx).Bytes(), privateKey) + if err != nil { + return nil, err + } + return tx.WithSignature(signer, signature) + }, nil +} + +func KeyStoreSignerFn(path string, password string, chainID *big.Int) (bind.SignerFn, error) { + privateKey, err := sdkEcdsa.ReadKey(path, password) + if err != nil { + return nil, err + } + return PrivateKeySignerFn(privateKey, chainID) +} + +func RemoteSignerFn(address common.Address, chainID *big.Int) (bind.SignerFn, error) { + return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { + return nil, errors.New("unimplemented") + }, errors.New("unimplemented") +} + +func SignerFromConfig(c Config, chainID *big.Int) (SignerFn, common.Address, error) { + var signer SignerFn + var senderAddress common.Address + var err error + if c.IsPrivateKeySigner() { + senderAddress = crypto.PubkeyToAddress(c.PrivateKey.PublicKey) + signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { + return PrivateKeySignerFn(c.PrivateKey, chainID) + } + } else if c.IsLocalKeystoreSigner() { + senderAddress, err = sdkEcdsa.GetAddressFromKeyStoreFile(c.KeystorePath) + if err != nil { + return nil, common.Address{}, err + } + signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { + return KeyStoreSignerFn(c.KeystorePath, c.Password, chainID) + } + } else if c.IsRemoteSigner() { + signer = func(ctx context.Context, address common.Address) (bind.SignerFn, error) { + return RemoteSignerFn(address, chainID) + } + } else { + return nil, common.Address{}, errors.New("no signer found") + } + if err != nil { + return nil, common.Address{}, err + } + return signer, senderAddress, nil +}