diff --git a/Makefile b/Makefile index e36cf383..08b4b02d 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ mocks: ## generates mocks .PHONY: tests tests: ## runs all tests - go test -race ./... -timeout=4m + go test -race ./... -timeout=5m .PHONY: tests-cover tests-cover: ## run all tests with test coverge diff --git a/chainio/clients/avsregistry/bindings.go b/chainio/clients/avsregistry/bindings.go index 60bce630..5ddf9b5f 100644 --- a/chainio/clients/avsregistry/bindings.go +++ b/chainio/clients/avsregistry/bindings.go @@ -38,91 +38,6 @@ type ContractBindings struct { OperatorStateRetriever *opstateretriever.ContractOperatorStateRetriever } -// NewAVSRegistryContractBindings creates a new instance of ContractBindings -// Deprecated: Use NewBindingsFromConfig instead -func NewAVSRegistryContractBindings( - registryCoordinatorAddr gethcommon.Address, - operatorStateRetrieverAddr gethcommon.Address, - ethclient eth.HttpBackend, - logger logging.Logger, -) (*ContractBindings, error) { - contractBlsRegistryCoordinator, err := regcoordinator.NewContractRegistryCoordinator( - registryCoordinatorAddr, - ethclient, - ) - if err != nil { - return nil, utils.WrapError("Failed to create BLSRegistryCoordinator contract", err) - } - - serviceManagerAddr, err := contractBlsRegistryCoordinator.ServiceManager(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to fetch ServiceManager address", err) - } - contractServiceManager, err := servicemanager.NewContractServiceManagerBase( - serviceManagerAddr, - ethclient, - ) - if err != nil { - return nil, utils.WrapError("Failed to fetch ServiceManager contract", err) - } - - stakeregistryAddr, err := contractBlsRegistryCoordinator.StakeRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to fetch StakeRegistry address", err) - } - contractStakeRegistry, err := stakeregistry.NewContractStakeRegistry( - stakeregistryAddr, - ethclient, - ) - if err != nil { - return nil, utils.WrapError("Failed to fetch StakeRegistry contract", err) - } - - blsApkRegistryAddr, err := contractBlsRegistryCoordinator.BlsApkRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to fetch BLSPubkeyRegistry address", err) - } - contractBlsApkRegistry, err := blsapkregistry.NewContractBLSApkRegistry( - blsApkRegistryAddr, - ethclient, - ) - if err != nil { - return nil, utils.WrapError("Failed to fetch BLSPubkeyRegistry contract", err) - } - - indexRegistryAddr, err := contractBlsRegistryCoordinator.IndexRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to fetch IndexRegistry address", err) - } - contractIndexRegistry, err := indexregistry.NewContractIndexRegistry(indexRegistryAddr, ethclient) - if err != nil { - return nil, utils.WrapError("Failed to fetch IndexRegistry contract", err) - } - - contractOperatorStateRetriever, err := opstateretriever.NewContractOperatorStateRetriever( - operatorStateRetrieverAddr, - ethclient, - ) - if err != nil { - return nil, utils.WrapError("Failed to fetch OperatorStateRetriever contract", err) - } - - return &ContractBindings{ - ServiceManagerAddr: serviceManagerAddr, - RegistryCoordinatorAddr: registryCoordinatorAddr, - StakeRegistryAddr: stakeregistryAddr, - BlsApkRegistryAddr: blsApkRegistryAddr, - IndexRegistryAddr: indexRegistryAddr, - OperatorStateRetrieverAddr: operatorStateRetrieverAddr, - ServiceManager: contractServiceManager, - RegistryCoordinator: contractBlsRegistryCoordinator, - StakeRegistry: contractStakeRegistry, - BlsApkRegistry: contractBlsApkRegistry, - IndexRegistry: contractIndexRegistry, - OperatorStateRetriever: contractOperatorStateRetriever, - }, nil -} - // NewBindingsFromConfig creates a new instance of ContractBindings func NewBindingsFromConfig( cfg Config, diff --git a/chainio/clients/avsregistry/reader.go b/chainio/clients/avsregistry/reader.go index f4ba2156..382cb27b 100644 --- a/chainio/clients/avsregistry/reader.go +++ b/chainio/clients/avsregistry/reader.go @@ -84,48 +84,6 @@ func NewReaderFromConfig( ), nil } -// BuildAvsRegistryChainReader creates a new ChainReader -// Deprecated: Use NewReaderFromConfig instead -func BuildAvsRegistryChainReader( - registryCoordinatorAddr common.Address, - operatorStateRetrieverAddr common.Address, - ethClient eth.HttpBackend, - logger logging.Logger, -) (*ChainReader, error) { - contractRegistryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create contractRegistryCoordinator", err) - } - blsApkRegistryAddr, err := contractRegistryCoordinator.BlsApkRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get blsApkRegistryAddr", err) - } - stakeRegistryAddr, err := contractRegistryCoordinator.StakeRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get stakeRegistryAddr", err) - } - contractStakeRegistry, err := stakeregistry.NewContractStakeRegistry(stakeRegistryAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create contractStakeRegistry", err) - } - contractOperatorStateRetriever, err := opstateretriever.NewContractOperatorStateRetriever( - operatorStateRetrieverAddr, - ethClient, - ) - if err != nil { - return nil, utils.WrapError("Failed to create contractOperatorStateRetriever", err) - } - return NewChainReader( - registryCoordinatorAddr, - blsApkRegistryAddr, - contractRegistryCoordinator, - contractOperatorStateRetriever, - contractStakeRegistry, - logger, - ethClient, - ), nil -} - func (r *ChainReader) GetQuorumCount(opts *bind.CallOpts) (uint8, error) { if r.registryCoordinator == nil { return 0, errors.New("RegistryCoordinator contract not provided") diff --git a/chainio/clients/avsregistry/subscriber.go b/chainio/clients/avsregistry/subscriber.go index 70d188b7..abd9e08b 100644 --- a/chainio/clients/avsregistry/subscriber.go +++ b/chainio/clients/avsregistry/subscriber.go @@ -2,7 +2,6 @@ package avsregistry import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" @@ -34,28 +33,6 @@ func NewChainSubscriber( } } -// BuildAvsRegistryChainSubscriber creates a new instance of ChainSubscriber -// Deprecated: Use NewSubscriberFromConfig instead -func BuildAvsRegistryChainSubscriber( - regCoordAddr common.Address, - ethWsClient eth.WsBackend, - logger logging.Logger, -) (*ChainSubscriber, error) { - regCoord, err := regcoord.NewContractRegistryCoordinator(regCoordAddr, ethWsClient) - if err != nil { - return nil, utils.WrapError("Failed to create RegistryCoordinator contract", err) - } - blsApkRegAddr, err := regCoord.BlsApkRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get BLSApkRegistry address from RegistryCoordinator", err) - } - blsApkReg, err := blsapkreg.NewContractBLSApkRegistry(blsApkRegAddr, ethWsClient) - if err != nil { - return nil, utils.WrapError("Failed to create BLSApkRegistry contract", err) - } - return NewChainSubscriber(regCoord, blsApkReg, logger), nil -} - // NewSubscriberFromConfig creates a new instance of ChainSubscriber // A websocket ETH Client must be provided func NewSubscriberFromConfig( diff --git a/chainio/clients/avsregistry/writer.go b/chainio/clients/avsregistry/writer.go index 8f54834e..99e5ec1a 100644 --- a/chainio/clients/avsregistry/writer.go +++ b/chainio/clients/avsregistry/writer.go @@ -19,12 +19,10 @@ import ( blsapkregistry "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" opstateretriever "github.com/Layr-Labs/eigensdk-go/contracts/bindings/OperatorStateRetriever" regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" - smbase "github.com/Layr-Labs/eigensdk-go/contracts/bindings/ServiceManagerBase" stakeregistry "github.com/Layr-Labs/eigensdk-go/contracts/bindings/StakeRegistry" "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/types" - "github.com/Layr-Labs/eigensdk-go/utils" ) type eLReader interface { @@ -75,75 +73,6 @@ func NewChainWriter( } } -// BuildAvsRegistryChainWriter creates a new ChainWriter instance from the provided contract addresses -// Deprecated: Use NewWriterFromConfig instead -func BuildAvsRegistryChainWriter( - registryCoordinatorAddr gethcommon.Address, - operatorStateRetrieverAddr gethcommon.Address, - logger logging.Logger, - ethClient eth.HttpBackend, - txMgr txmgr.TxManager, -) (*ChainWriter, error) { - registryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create RegistryCoordinator contract", err) - } - operatorStateRetriever, err := opstateretriever.NewContractOperatorStateRetriever( - operatorStateRetrieverAddr, - ethClient, - ) - if err != nil { - return nil, utils.WrapError("Failed to create OperatorStateRetriever contract", err) - } - serviceManagerAddr, err := registryCoordinator.ServiceManager(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get ServiceManager address", err) - } - serviceManager, err := smbase.NewContractServiceManagerBase(serviceManagerAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create ServiceManager contract", err) - } - blsApkRegistryAddr, err := registryCoordinator.BlsApkRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get BLSApkRegistry address", err) - } - blsApkRegistry, err := blsapkregistry.NewContractBLSApkRegistry(blsApkRegistryAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create BLSApkRegistry contract", err) - } - stakeRegistryAddr, err := registryCoordinator.StakeRegistry(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get StakeRegistry address", err) - } - stakeRegistry, err := stakeregistry.NewContractStakeRegistry(stakeRegistryAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create StakeRegistry contract", err) - } - delegationManagerAddr, err := stakeRegistry.Delegation(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get DelegationManager address", err) - } - avsDirectoryAddr, err := serviceManager.AvsDirectory(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to get AvsDirectory address", err) - } - elReader, err := elcontracts.BuildELChainReader(delegationManagerAddr, avsDirectoryAddr, ethClient, logger) - if err != nil { - return nil, utils.WrapError("Failed to create ELChainReader", err) - } - return NewChainWriter( - serviceManagerAddr, - registryCoordinator, - operatorStateRetriever, - stakeRegistry, - blsApkRegistry, - elReader, - logger, - ethClient, - txMgr, - ), nil -} - // NewWriterFromConfig creates a new ChainWriter from the provided config func NewWriterFromConfig( cfg Config, diff --git a/chainio/clients/avsregistry/writer_test.go b/chainio/clients/avsregistry/writer_test.go index ed775962..8df78ec3 100644 --- a/chainio/clients/avsregistry/writer_test.go +++ b/chainio/clients/avsregistry/writer_test.go @@ -11,6 +11,7 @@ import ( "github.com/Layr-Labs/eigensdk-go/types" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -72,3 +73,37 @@ func TestWriterMethods(t *testing.T) { require.NotNil(t, receipt) }) } + +// Compliance test for BLS signature +func TestBlsSignature(t *testing.T) { + // read input from JSON if available, otherwise use default values + // Data taken from + // https://github.com/Layr-Labs/eigensdk-compliance/blob/429459572302d9fd42c1184b7257703460ba09ca/data_files/bls_signature.json + var defaultInput = struct { + Message string `json:"message"` + BlsPrivKey string `json:"bls_priv_key"` + }{ + Message: "Hello, world!Hello, world!123456", + BlsPrivKey: "12248929636257230549931416853095037629726205319386239410403476017439825112537", + } + + testData := testutils.NewTestData(defaultInput) + // The message to sign + messageArray := []byte(testData.Input.Message) + + var messageArray32 [32]byte + copy(messageArray32[:], messageArray) + + // The private key as a string + privKey, _ := bls.NewPrivateKey(testData.Input.BlsPrivKey) + keyPair := bls.NewKeyPair(privKey) + + sig := keyPair.SignMessage(messageArray32) + + x := sig.G1Affine.X.String() + y := sig.G1Affine.Y.String() + + // Values taken from previous run of this test + assert.Equal(t, x, "15790168376429033610067099039091292283117017641532256477437243974517959682102") + assert.Equal(t, y, "4960450323239587206117776989095741074887370703941588742100855592356200866613") +} diff --git a/chainio/clients/builder.go b/chainio/clients/builder.go index 3f7311dc..7475fc97 100644 --- a/chainio/clients/builder.go +++ b/chainio/clients/builder.go @@ -3,6 +3,7 @@ package clients import ( "context" "crypto/ecdsa" + "fmt" "time" "github.com/ethereum/go-ethereum/ethclient" @@ -56,7 +57,10 @@ func BuildReadClients( config BuildAllConfig, logger logging.Logger, ) (*ReadClients, error) { - config.validate(logger) + err := config.validate(logger) + if err != nil { + return nil, utils.WrapError("Failed to validate logger", err) + } // Create the metrics server promReg := prometheus.NewRegistry() @@ -127,7 +131,10 @@ func BuildAll( ecdsaPrivateKey *ecdsa.PrivateKey, logger logging.Logger, ) (*Clients, error) { - config.validate(logger) + err := config.validate(logger) + if err != nil { + return nil, utils.WrapError("Failed to validate logger", err) + } // Create the metrics server promReg := prometheus.NewRegistry() @@ -148,7 +155,8 @@ func BuildAll( defer cancel() chainid, err := ethHttpClient.ChainID(rpcCtx) if err != nil { - logger.Fatal("Cannot get chain id", "err", err) + logger.Error("Cannot get chain id", "err", err) + return nil, utils.WrapError("Cannot get chain id", err) } signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid) if err != nil { @@ -215,23 +223,30 @@ func BuildAll( // Very basic validation that makes sure all fields are nonempty // we might eventually want more sophisticated validation, based on regexp, // or use something like https://json-schema.org/ (?) -func (config *BuildAllConfig) validate(logger logging.Logger) { +func (config *BuildAllConfig) validate(logger logging.Logger) error { if config.EthHttpUrl == "" { - logger.Fatalf("BuildAllConfig.validate: Missing eth http url") + logger.Error("BuildAllConfig.validate: Missing eth http url") + return fmt.Errorf("BuildAllConfig.validate: Missing eth http url") } if config.EthWsUrl == "" { - logger.Fatalf("BuildAllConfig.validate: Missing eth ws url") + logger.Error("BuildAllConfig.validate: Missing eth ws url") + return fmt.Errorf("BuildAllConfig.validate: Missing eth ws url") } if config.RegistryCoordinatorAddr == "" { - logger.Fatalf("BuildAllConfig.validate: Missing bls registry coordinator address") + logger.Error("BuildAllConfig.validate: Missing bls registry coordinator address") + return fmt.Errorf("BuildAllConfig.validate: Missing bls registry coordinator address") } if config.OperatorStateRetrieverAddr == "" { - logger.Fatalf("BuildAllConfig.validate: Missing bls operator state retriever address") + logger.Error("BuildAllConfig.validate: Missing bls operator state retriever address") + return fmt.Errorf("BuildAllConfig.validate: Missing bls operator state retriever address") } if config.AvsName == "" { - logger.Fatalf("BuildAllConfig.validate: Missing avs name") + logger.Error("BuildAllConfig.validate: Missing avs name") + return fmt.Errorf("BuildAllConfig.validate: Missing avs name") } if config.PromMetricsIpPortAddress == "" { - logger.Fatalf("BuildAllConfig.validate: Missing prometheus metrics ip port address") + logger.Error("BuildAllConfig.validate: Missing prometheus metrics ip port address") + return fmt.Errorf("BuildAllConfig.validate: Missing prometheus metrics ip port address") } + return nil } diff --git a/chainio/clients/elcontracts/bindings.go b/chainio/clients/elcontracts/bindings.go index cea41aca..8c6639ad 100644 --- a/chainio/clients/elcontracts/bindings.go +++ b/chainio/clients/elcontracts/bindings.go @@ -127,40 +127,3 @@ func NewBindingsFromConfig( func isZeroAddress(address gethcommon.Address) bool { return address == gethcommon.Address{} } - -// NewEigenlayerContractBindings creates a new ContractBindings struct with the provided contract addresses -// Deprecated: Use NewBindingsFromConfig instead -func NewEigenlayerContractBindings( - delegationManagerAddr gethcommon.Address, - avsDirectoryAddr gethcommon.Address, - ethclient eth.HttpBackend, - logger logging.Logger, -) (*ContractBindings, error) { - contractDelegationManager, err := delegationmanager.NewContractDelegationManager(delegationManagerAddr, ethclient) - if err != nil { - return nil, utils.WrapError("Failed to create DelegationManager contract", err) - } - - strategyManagerAddr, err := contractDelegationManager.StrategyManager(&bind.CallOpts{}) - if err != nil { - return nil, utils.WrapError("Failed to fetch StrategyManager address", err) - } - contractStrategyManager, err := strategymanager.NewContractStrategyManager(strategyManagerAddr, ethclient) - if err != nil { - return nil, utils.WrapError("Failed to fetch StrategyManager contract", err) - } - - avsDirectory, err := avsdirectory.NewContractAVSDirectory(avsDirectoryAddr, ethclient) - if err != nil { - return nil, utils.WrapError("Failed to fetch AVSDirectory contract", err) - } - - return &ContractBindings{ - StrategyManagerAddr: strategyManagerAddr, - DelegationManagerAddr: delegationManagerAddr, - AvsDirectoryAddr: avsDirectoryAddr, - StrategyManager: contractStrategyManager, - DelegationManager: contractDelegationManager, - AvsDirectory: avsDirectory, - }, nil -} diff --git a/chainio/clients/elcontracts/reader.go b/chainio/clients/elcontracts/reader.go index c4e12ba7..972b627d 100644 --- a/chainio/clients/elcontracts/reader.go +++ b/chainio/clients/elcontracts/reader.go @@ -66,35 +66,6 @@ func NewChainReader( } } -// BuildELChainReader creates a new ELChainReader -// Deprecated: Use BuildFromConfig instead -func BuildELChainReader( - delegationManagerAddr gethcommon.Address, - avsDirectoryAddr gethcommon.Address, - ethClient eth.HttpBackend, - logger logging.Logger, -) (*ChainReader, error) { - elContractBindings, err := NewEigenlayerContractBindings( - delegationManagerAddr, - avsDirectoryAddr, - ethClient, - logger, - ) - if err != nil { - return nil, err - } - return NewChainReader( - elContractBindings.DelegationManager, - elContractBindings.StrategyManager, - elContractBindings.AvsDirectory, - elContractBindings.RewardsCoordinator, - elContractBindings.AllocationManager, - elContractBindings.PermissionController, - logger, - ethClient, - ), nil -} - func NewReaderFromConfig( cfg Config, ethClient eth.HttpBackend, diff --git a/chainio/clients/elcontracts/reader_test.go b/chainio/clients/elcontracts/reader_test.go index 34329455..292a42a1 100644 --- a/chainio/clients/elcontracts/reader_test.go +++ b/chainio/clients/elcontracts/reader_test.go @@ -10,6 +10,7 @@ import ( allocationmanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/AllocationManager" erc20 "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IERC20" rewardscoordinator "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IRewardsCoordinator" + "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/testutils" "github.com/Layr-Labs/eigensdk-go/testutils/testclients" @@ -652,7 +653,6 @@ func TestAdminFunctions(t *testing.T) { listPendingAdmins, err := chainReader.ListPendingAdmins(context.Background(), operatorAddr) assert.NoError(t, err) assert.NotEmpty(t, listPendingAdmins) - assert.Len(t, listPendingAdmins, 1) }) t.Run("non-existent admin", func(t *testing.T) { @@ -747,6 +747,224 @@ func TestAppointeesFunctions(t *testing.T) { }) } +func TestOperatorSetsAndSlashableShares(t *testing.T) { + testConfig := testutils.GetDefaultTestConfig() + anvilC, err := testutils.StartAnvilContainer(testConfig.AnvilStateFileName) + require.NoError(t, err) + + anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") + require.NoError(t, err) + contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) + + config := elcontracts.Config{ + DelegationManagerAddress: contractAddrs.DelegationManager, + } + chainReader, err := testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) + require.NoError(t, err) + + operatorAddr := common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS) + operatorPrivateKeyHex := testutils.ANVIL_SECOND_PRIVATE_KEY + chainWriter, err := testclients.NewTestChainWriterFromConfig(anvilHttpEndpoint, operatorPrivateKeyHex, config) + require.NoError(t, err) + + avsAdrr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) + avsPrivateKeyHex := testutils.ANVIL_FIRST_PRIVATE_KEY + operatorSetId := uint32(1) + operatorSet := allocationmanager.OperatorSet{ + Avs: avsAdrr, + Id: operatorSetId, + } + + strategyAddr := contractAddrs.Erc20MockStrategy + strategies := []common.Address{strategyAddr} + + err = createOperatorSet(anvilHttpEndpoint, avsPrivateKeyHex, avsAdrr, operatorSetId, strategyAddr) + require.NoError(t, err) + + keypair, err := bls.NewKeyPairFromString("0x01") + require.NoError(t, err) + + request := elcontracts.RegistrationRequest{ + OperatorAddress: operatorAddr, + AVSAddress: avsAdrr, + OperatorSetIds: []uint32{operatorSetId}, + WaitForReceipt: true, + Socket: "socket", + BlsKeyPair: keypair, + } + + registryCoordinatorAddress := contractAddrs.RegistryCoordinator + receipt, err := chainWriter.RegisterForOperatorSets( + context.Background(), + registryCoordinatorAddress, + request, + ) + require.NoError(t, err) + require.Equal(t, uint64(1), receipt.Status) + + allocationDelay := 1 + allocationMagnitude := 100 + allocationConfigurationDelay := 1200 + + receipt, err = chainWriter.SetAllocationDelay( + context.Background(), + operatorAddr, + uint32(allocationDelay), + true, + ) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + + testutils.AdvanceChainByNBlocksExecInContainer( + context.Background(), + allocationConfigurationDelay+1, + anvilC, + ) + + allocationParams := []allocationmanager.IAllocationManagerTypesAllocateParams{ + { + OperatorSet: operatorSet, + Strategies: strategies, + NewMagnitudes: []uint64{uint64(allocationMagnitude)}, + }, + } + + receipt, err = chainWriter.ModifyAllocations( + context.Background(), + operatorAddr, + allocationParams, + true, + ) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + + t.Run("get operators and operator sets", func(t *testing.T) { + t.Run("validate strategies for operatorSet", func(t *testing.T) { + strats, err := chainReader.GetStrategiesForOperatorSet(context.Background(), operatorSet) + require.NoError(t, err) + require.Len(t, strats, 1) + require.Equal(t, strats[0].Hex(), strategyAddr.Hex()) + }) + + t.Run("get registered sets", func(t *testing.T) { + registeredSets, err := chainReader.GetRegisteredSets(context.Background(), operatorAddr) + require.NoError(t, err) + require.NotEmpty(t, registeredSets) + }) + + t.Run("get operator sets for operator", func(t *testing.T) { + opSets, err := chainReader.GetOperatorSetsForOperator(context.Background(), operatorAddr) + require.NoError(t, err) + require.NotEmpty(t, opSets) + }) + + t.Run("get amount operatorSets for operator", func(t *testing.T) { + opSetsCount, err := chainReader.GetNumOperatorSetsForOperator( + context.Background(), + operatorAddr, + ) + require.NoError(t, err) + require.NotZero(t, opSetsCount) + }) + + t.Run("get operator for operatorsets", func(t *testing.T) { + operators, err := chainReader.GetOperatorsForOperatorSet(context.Background(), operatorSet) + require.NoError(t, err) + require.NotEmpty(t, operators) + }) + + t.Run("get amount of operators for operatorsets", func(t *testing.T) { + operatorsCount, err := chainReader.GetNumOperatorsForOperatorSet(context.Background(), operatorSet) + require.NoError(t, err) + require.NotZero(t, operatorsCount) + }) + }) + + t.Run("slashable shares tests", func(t *testing.T) { + t.Run("get slashable shares for single operator", func(t *testing.T) { + shares, err := chainReader.GetSlashableShares( + context.Background(), + operatorAddr, + operatorSet, + strategies, + ) + require.NoError(t, err) + require.NotEmpty(t, shares) + }) + + t.Run("get slashable shares for multiple operatorSets", func(t *testing.T) { + shares, err := chainReader.GetSlashableSharesForOperatorSets( + context.Background(), + []allocationmanager.OperatorSet{operatorSet}, + ) + require.NoError(t, err) + require.NotEmpty(t, shares) + }) + + t.Run("get slashable shares before specific block", func(t *testing.T) { + shares, err := chainReader.GetSlashableSharesForOperatorSetsBefore( + context.Background(), + []allocationmanager.OperatorSet{operatorSet}, + 2, + ) + require.NoError(t, err) + require.NotEmpty(t, shares) + }) + }) +} + +func TestOperatorSetsWithWrongInput(t *testing.T) { + _, anvilHttpEndpoint := testclients.BuildTestClients(t) + ctx := context.Background() + + contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) + operatorAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) + + config := elcontracts.Config{} + operatorSet := allocationmanager.OperatorSet{ + Avs: common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS), + Id: 0, + } + + chainReader, err := testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) + require.NoError(t, err) + + t.Run("test operator set with invalid id", func(t *testing.T) { + _, err := chainReader.GetOperatorsForOperatorSet(ctx, operatorSet) + require.Error(t, err) + + _, err = chainReader.GetNumOperatorsForOperatorSet(ctx, operatorSet) + require.Error(t, err) + + _, err = chainReader.GetStrategiesForOperatorSet(ctx, operatorSet) + require.Error(t, err) + + strategies := []common.Address{contractAddrs.Erc20MockStrategy} + + _, err = chainReader.GetSlashableShares( + ctx, + operatorAddr, + operatorSet, + strategies, + ) + require.Error(t, err) + }) + + t.Run("get slashable shares with invalid operatorSet", func(t *testing.T) { + config := elcontracts.Config{ + DelegationManagerAddress: contractAddrs.DelegationManager, + } + + chainReader, err = testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) + require.NoError(t, err) + + operatorSets := []allocationmanager.OperatorSet{operatorSet} + + _, err = chainReader.GetSlashableSharesForOperatorSetsBefore(context.Background(), operatorSets, 10) + require.Error(t, err) + }) +} + func TestCreateRederFromConfig(t *testing.T) { _, anvilHttpEndpoint := testclients.BuildTestClients(t) testConfig := testutils.GetDefaultTestConfig() diff --git a/chainio/clients/elcontracts/types.go b/chainio/clients/elcontracts/types.go index 4414dd07..21dd8ea4 100644 --- a/chainio/clients/elcontracts/types.go +++ b/chainio/clients/elcontracts/types.go @@ -4,6 +4,7 @@ import ( "math/big" allocationmanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/AllocationManager" + "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/ethereum/go-ethereum/common" ) @@ -39,8 +40,9 @@ type RegistrationRequest struct { AVSAddress common.Address OperatorSetIds []uint32 WaitForReceipt bool + BlsKeyPair *bls.KeyPair + Socket string } - type RemovePermissionRequest struct { AccountAddress common.Address AppointeeAddress common.Address diff --git a/chainio/clients/elcontracts/writer.go b/chainio/clients/elcontracts/writer.go index 654bc394..fc95ee6c 100644 --- a/chainio/clients/elcontracts/writer.go +++ b/chainio/clients/elcontracts/writer.go @@ -4,6 +4,7 @@ import ( "context" "errors" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "math/big" @@ -13,6 +14,7 @@ import ( "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" avsdirectory "github.com/Layr-Labs/eigensdk-go/contracts/bindings/AVSDirectory" allocationmanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/AllocationManager" delegationmanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/DelegationManager" @@ -20,7 +22,9 @@ import ( rewardscoordinator "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IRewardsCoordinator" strategy "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IStrategy" permissioncontroller "github.com/Layr-Labs/eigensdk-go/contracts/bindings/PermissionController" + regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" strategymanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/StrategyManager" + "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/types" @@ -78,51 +82,6 @@ func NewChainWriter( } } -// BuildELChainWriter builds an ChainWriter instance. -// Deprecated: Use NewWriterFromConfig instead. -func BuildELChainWriter( - delegationManagerAddr gethcommon.Address, - avsDirectoryAddr gethcommon.Address, - ethClient eth.HttpBackend, - logger logging.Logger, - eigenMetrics metrics.Metrics, - txMgr txmgr.TxManager, -) (*ChainWriter, error) { - elContractBindings, err := NewEigenlayerContractBindings( - delegationManagerAddr, - avsDirectoryAddr, - ethClient, - logger, - ) - if err != nil { - return nil, err - } - elChainReader := NewChainReader( - elContractBindings.DelegationManager, - elContractBindings.StrategyManager, - elContractBindings.AvsDirectory, - elContractBindings.RewardsCoordinator, - elContractBindings.AllocationManager, - elContractBindings.PermissionController, - logger, - ethClient, - ) - return NewChainWriter( - elContractBindings.DelegationManager, - elContractBindings.StrategyManager, - elContractBindings.RewardsCoordinator, - elContractBindings.AvsDirectory, - elContractBindings.AllocationManager, - elContractBindings.PermissionController, - elContractBindings.StrategyManagerAddr, - elChainReader, - ethClient, - logger, - eigenMetrics, - txMgr, - ), nil -} - func NewWriterFromConfig( cfg Config, ethClient eth.HttpBackend, @@ -579,6 +538,7 @@ func (w *ChainWriter) DeregisterFromOperatorSets( func (w *ChainWriter) RegisterForOperatorSets( ctx context.Context, + registryCoordinatorAddr gethcommon.Address, request RegistrationRequest, ) (*gethtypes.Receipt, error) { if w.allocationManager == nil { @@ -590,12 +550,27 @@ func (w *ChainWriter) RegisterForOperatorSets( return nil, utils.WrapError("failed to get no send tx opts", err) } + pubkeyRegParams, err := getPubkeyRegistrationParams( + w.ethClient, + registryCoordinatorAddr, + request.OperatorAddress, + request.BlsKeyPair, + ) + if err != nil { + return nil, utils.WrapError("failed to get public key registration params", err) + } + + data, err := abiEncodeRegistrationParams(request.Socket, *pubkeyRegParams) + if err != nil { + return nil, utils.WrapError("failed to encode registration params", err) + } tx, err := w.allocationManager.RegisterForOperatorSets( noSendTxOpts, request.OperatorAddress, allocationmanager.IAllocationManagerTypesRegisterParams{ Avs: request.AVSAddress, OperatorSetIds: request.OperatorSetIds, + Data: data, }) if err != nil { return nil, utils.WrapError("failed to create RegisterForOperatorSets tx", err) @@ -774,3 +749,79 @@ func (w *ChainWriter) RemovePendingAdmin( return w.txMgr.Send(ctx, tx, request.WaitForReceipt) } + +func getPubkeyRegistrationParams( + ethClient bind.ContractBackend, + registryCoordinatorAddr, operatorAddress gethcommon.Address, + blsKeyPair *bls.KeyPair, +) (*regcoord.IBLSApkRegistryPubkeyRegistrationParams, error) { + registryCoordinator, err := regcoord.NewContractRegistryCoordinator(registryCoordinatorAddr, ethClient) + if err != nil { + return nil, utils.WrapError("failed to create registry coordinator", err) + } + // params to register bls pubkey with bls apk registry + g1HashedMsgToSign, err := registryCoordinator.PubkeyRegistrationMessageHash( + &bind.CallOpts{}, + operatorAddress, + ) + if err != nil { + return nil, err + } + signedMsg := chainioutils.ConvertToBN254G1Point( + blsKeyPair.SignHashedToCurveMessage(chainioutils.ConvertBn254GethToGnark(g1HashedMsgToSign)).G1Point, + ) + G1pubkeyBN254 := chainioutils.ConvertToBN254G1Point(blsKeyPair.GetPubKeyG1()) + G2pubkeyBN254 := chainioutils.ConvertToBN254G2Point(blsKeyPair.GetPubKeyG2()) + pubkeyRegParams := regcoord.IBLSApkRegistryPubkeyRegistrationParams{ + PubkeyRegistrationSignature: signedMsg, + PubkeyG1: G1pubkeyBN254, + PubkeyG2: G2pubkeyBN254, + } + return &pubkeyRegParams, nil +} + +func abiEncodeRegistrationParams( + socket string, + pubkeyRegistrationParams regcoord.IBLSApkRegistryPubkeyRegistrationParams, +) ([]byte, error) { + registrationParamsType, err := abi.NewType("tuple", "", []abi.ArgumentMarshaling{ + {Name: "Socket", Type: "string"}, + {Name: "PubkeyRegParams", Type: "tuple", Components: []abi.ArgumentMarshaling{ + {Name: "PubkeyRegistrationSignature", Type: "tuple", Components: []abi.ArgumentMarshaling{ + {Name: "X", Type: "uint256"}, + {Name: "Y", Type: "uint256"}, + }}, + {Name: "PubkeyG1", Type: "tuple", Components: []abi.ArgumentMarshaling{ + {Name: "X", Type: "uint256"}, + {Name: "Y", Type: "uint256"}, + }}, + {Name: "PubkeyG2", Type: "tuple", Components: []abi.ArgumentMarshaling{ + {Name: "X", Type: "uint256[2]"}, + {Name: "Y", Type: "uint256[2]"}, + }}, + }}, + }) + if err != nil { + return nil, err + } + + registrationParams := struct { + Socket string + PubkeyRegParams regcoord.IBLSApkRegistryPubkeyRegistrationParams + }{ + socket, + pubkeyRegistrationParams, + } + + args := abi.Arguments{ + {Type: registrationParamsType, Name: "registrationParams"}, + } + + data, err := args.Pack(®istrationParams) + if err != nil { + return nil, err + } + // The encoder is prepending 32 bytes to the data as if it was used in a dynamic function parameter. + // This is not used when decoding the bytes directly, so we need to remove it. + return data[32:], nil +} diff --git a/chainio/clients/elcontracts/writer_test.go b/chainio/clients/elcontracts/writer_test.go index 1e95d1c0..6be122b3 100644 --- a/chainio/clients/elcontracts/writer_test.go +++ b/chainio/clients/elcontracts/writer_test.go @@ -10,6 +10,8 @@ import ( "github.com/Layr-Labs/eigensdk-go/chainio/clients" "github.com/Layr-Labs/eigensdk-go/chainio/clients/elcontracts" allocationmanager "github.com/Layr-Labs/eigensdk-go/contracts/bindings/AllocationManager" + "github.com/Layr-Labs/eigensdk-go/crypto/bls" + rewardscoordinator "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IRewardsCoordinator" strategy "github.com/Layr-Labs/eigensdk-go/contracts/bindings/IStrategy" mockerc20 "github.com/Layr-Labs/eigensdk-go/contracts/bindings/MockERC20" @@ -119,6 +121,125 @@ func TestRegisterOperator(t *testing.T) { }) } +func TestRegisterAndDeregisterFromOperatorSets(t *testing.T) { + testConfig := testutils.GetDefaultTestConfig() + anvilC, err := testutils.StartAnvilContainer(testConfig.AnvilStateFileName) + require.NoError(t, err) + + anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") + require.NoError(t, err) + contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) + + operatorAddressHex := testutils.ANVIL_SECOND_ADDRESS + operatorPrivateKeyHex := testutils.ANVIL_SECOND_PRIVATE_KEY + + config := elcontracts.Config{ + DelegationManagerAddress: contractAddrs.DelegationManager, + RewardsCoordinatorAddress: contractAddrs.RewardsCoordinator, + } + + // Create operator clients + chainWriter, err := testclients.NewTestChainWriterFromConfig(anvilHttpEndpoint, operatorPrivateKeyHex, config) + require.NoError(t, err) + + chainReader, err := testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) + require.NoError(t, err) + + avsAddress := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) + operatorSetId := uint32(1) + erc20MockStrategyAddr := contractAddrs.Erc20MockStrategy + + // Create an operator set to register an operator on it + err = createOperatorSet( + anvilHttpEndpoint, + testutils.ANVIL_FIRST_PRIVATE_KEY, + avsAddress, + operatorSetId, + erc20MockStrategyAddr, + ) + require.NoError(t, err) + + operatorAddress := common.HexToAddress(operatorAddressHex) + keypair, err := bls.NewKeyPairFromString("0x01") + require.NoError(t, err) + + request := elcontracts.RegistrationRequest{ + OperatorAddress: operatorAddress, + AVSAddress: avsAddress, + OperatorSetIds: []uint32{operatorSetId}, + WaitForReceipt: true, + Socket: "socket", + BlsKeyPair: keypair, + } + + operatorSet := allocationmanager.OperatorSet{ + Avs: avsAddress, + Id: uint32(operatorSetId), + } + t.Run("register operator for operator set", func(t *testing.T) { + registryCoordinatorAddress := contractAddrs.RegistryCoordinator + receipt, err := chainWriter.RegisterForOperatorSets( + context.Background(), + registryCoordinatorAddress, + request, + ) + + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + + isRegistered, err := chainReader.IsOperatorRegisteredWithOperatorSet( + context.Background(), + operatorAddress, + operatorSet, + ) + require.NoError(t, err) + require.Equal(t, true, isRegistered) + }) + + t.Run("register operator for same operator set", func(t *testing.T) { + registryCoordinatorAddress := contractAddrs.RegistryCoordinator + _, err = chainWriter.RegisterForOperatorSets( + context.Background(), + registryCoordinatorAddress, + request, + ) + require.Error(t, err, "cannot register an operator to an operator set that is already registered") + }) + + deregistrationRequest := elcontracts.DeregistrationRequest{ + AVSAddress: avsAddress, + OperatorSetIds: []uint32{operatorSetId}, + WaitForReceipt: true, + } + + t.Run("deregister operator from operator set", func(t *testing.T) { + receipt, err := chainWriter.DeregisterFromOperatorSets( + context.Background(), + operatorAddress, + deregistrationRequest, + ) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + + isRegistered, err := chainReader.IsOperatorRegisteredWithOperatorSet( + context.Background(), + operatorAddress, + operatorSet, + ) + require.NoError(t, err) + require.False(t, isRegistered) + }) + + t.Run("deregister operator from operator set when not registered", func(t *testing.T) { + _, err = chainWriter.DeregisterFromOperatorSets( + context.Background(), + operatorAddress, + deregistrationRequest, + ) + require.Error(t, err, "cannot deregister an operator that is not registered") + }) +} + func TestChainWriter(t *testing.T) { clients, anvilHttpEndpoint := testclients.BuildTestClients(t) contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) @@ -139,6 +260,20 @@ func TestChainWriter(t *testing.T) { assert.True(t, receipt.Status == 1) }) + t.Run("update operator details when address is not an operator", func(t *testing.T) { + wrongOperatorModified := types.Operator{ + Address: testutils.ANVIL_THIRD_ADDRESS, + DelegationApproverAddress: testutils.ANVIL_FIRST_ADDRESS, + MetadataUrl: "eigensdk-go", + } + _, err := clients.ElChainWriter.UpdateOperatorDetails( + context.Background(), + wrongOperatorModified, + true, + ) + assert.Error(t, err, "cannot update operator details for an address that is not an operator") + }) + t.Run("update metadata URI", func(t *testing.T) { walletModified, err := crypto.HexToECDSA(testutils.ANVIL_FIRST_PRIVATE_KEY) assert.NoError(t, err) @@ -153,6 +288,16 @@ func TestChainWriter(t *testing.T) { assert.True(t, receipt.Status == 1) }) + t.Run("update metadata URI when address is not an operator", func(t *testing.T) { + _, err := clients.ElChainWriter.UpdateMetadataURI( + context.Background(), + common.HexToAddress(testutils.ANVIL_THIRD_ADDRESS), + "https://0.0.0.0", + true, + ) + assert.Error(t, err, "cannot update metadata URI for an address that is not an operator") + }) + t.Run("deposit ERC20 into strategy", func(t *testing.T) { amount := big.NewInt(1) receipt, err := clients.ElChainWriter.DepositERC20IntoStrategy( @@ -242,6 +387,11 @@ func TestSetOperatorPISplit(t *testing.T) { updatedSplit, err := chainReader.GetOperatorPISplit(context.Background(), operatorAddr) require.NoError(t, err) require.Equal(t, newSplit, updatedSplit) + + // Set a invalid operator PI split + invalidSplit := uint16(10001) + _, err = chainWriter.SetOperatorPISplit(context.Background(), operatorAddr, invalidSplit, waitForReceipt) + require.Error(t, err, "split must be less than 10000") } func TestSetOperatorAVSSplit(t *testing.T) { @@ -299,6 +449,17 @@ func TestSetOperatorAVSSplit(t *testing.T) { updatedSplit, err := chainReader.GetOperatorAVSSplit(context.Background(), operatorAddr, avsAddr) require.NoError(t, err) require.Equal(t, newSplit, updatedSplit) + + // Set a invalid operator AVS split + invalidSplit := uint16(10001) + _, err = chainWriter.SetOperatorAVSSplit( + context.Background(), + operatorAddr, + avsAddr, + invalidSplit, + waitForReceipt, + ) + require.Error(t, err, "split must be less than 10000") } func TestSetAllocationDelay(t *testing.T) { @@ -325,10 +486,24 @@ func TestSetAllocationDelay(t *testing.T) { chainWriter, err := testclients.NewTestChainWriterFromConfig(anvilHttpEndpoint, privateKeyHex, config) require.NoError(t, err) - delay := uint32(10) - receipt, err := chainWriter.SetAllocationDelay(context.Background(), operatorAddr, delay, waitForReceipt) - require.NoError(t, err) - require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + t.Run("set allocation delay", func(t *testing.T) { + delay := uint32(10) + receipt, err := chainWriter.SetAllocationDelay(context.Background(), operatorAddr, delay, waitForReceipt) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + }) + + t.Run("set allocation delay with invalid caller", func(t *testing.T) { + invalidCaller := common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS) + delay := uint32(20) + _, err = chainWriter.SetAllocationDelay( + context.Background(), + invalidCaller, + delay, + waitForReceipt, + ) + require.Error(t, err, "cannot set allocation delay with an invalid caller") + }) } func TestSetAndRemovePermission(t *testing.T) { @@ -352,8 +527,8 @@ func TestSetAndRemovePermission(t *testing.T) { require.NoError(t, err) accountAddress := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) - appointeeAddress := common.HexToAddress("009440d62dc85c73dbf889b7ad1f4da8b231d2ef") - target := common.HexToAddress("14dC79964da2C08b23698B3D3cc7Ca32193d9955") + appointeeAddress := common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS) + target := common.HexToAddress(testutils.ANVIL_THIRD_ADDRESS) selector := [4]byte{0, 1, 2, 3} waitForReceipt := true @@ -372,21 +547,36 @@ func TestSetAndRemovePermission(t *testing.T) { Selector: selector, WaitForReceipt: waitForReceipt, } - receipt, err := chainWriter.SetPermission(context.Background(), setPermissionRequest) - require.NoError(t, err) - require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) - canCall, err := chainReader.CanCall(context.Background(), accountAddress, appointeeAddress, target, selector) - require.NoError(t, err) - require.True(t, canCall) + t.Run("set permission to account", func(t *testing.T) { + receipt, err := chainWriter.SetPermission(context.Background(), setPermissionRequest) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) - receipt, err = chainWriter.RemovePermission(context.Background(), removePermissionRequest) - require.NoError(t, err) - require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + canCall, err := chainReader.CanCall(context.Background(), accountAddress, appointeeAddress, target, selector) + require.NoError(t, err) + require.True(t, canCall) + }) - canCall, err = chainReader.CanCall(context.Background(), accountAddress, appointeeAddress, target, selector) - require.NoError(t, err) - require.False(t, canCall) + t.Run("set permission to account when already set", func(t *testing.T) { + _, err := chainWriter.SetPermission(context.Background(), setPermissionRequest) + require.Error(t, err, "cannot set a permission that has already been set") + }) + + t.Run("remove permission from account", func(t *testing.T) { + receipt, err := chainWriter.RemovePermission(context.Background(), removePermissionRequest) + require.NoError(t, err) + require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) + + canCall, err := chainReader.CanCall(context.Background(), accountAddress, appointeeAddress, target, selector) + require.NoError(t, err) + require.False(t, canCall) + }) + + t.Run("remove permission from account when not set", func(t *testing.T) { + _, err := chainWriter.RemovePermission(context.Background(), removePermissionRequest) + require.Error(t, err, "cannot remove a permission that has not been set") + }) } func TestModifyAllocations(t *testing.T) { @@ -414,6 +604,22 @@ func TestModifyAllocations(t *testing.T) { avsAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) operatorSetId := uint32(1) + operatorSet := allocationmanager.OperatorSet{ + Avs: avsAddr, + Id: operatorSetId, + } + newAllocation := uint64(100) + allocateParams := []allocationmanager.IAllocationManagerTypesAllocateParams{ + { + OperatorSet: operatorSet, + Strategies: []common.Address{strategyAddr}, + NewMagnitudes: []uint64{newAllocation}, + }, + } + + _, err = chainWriter.ModifyAllocations(context.Background(), operatorAddr, allocateParams, false) + require.Error(t, err, "cannot modify allocations without initializing the allocation delay") + waitForReceipt := true delay := uint32(1) // The allocation delay must be initialized before modifying the allocations @@ -433,19 +639,6 @@ func TestModifyAllocations(t *testing.T) { err = createOperatorSet(anvilHttpEndpoint, privateKeyHex, avsAddr, operatorSetId, strategyAddr) require.NoError(t, err) - operatorSet := allocationmanager.OperatorSet{ - Avs: avsAddr, - Id: operatorSetId, - } - newAllocation := uint64(100) - allocateParams := []allocationmanager.IAllocationManagerTypesAllocateParams{ - { - OperatorSet: operatorSet, - Strategies: []common.Address{strategyAddr}, - NewMagnitudes: []uint64{newAllocation}, - }, - } - receipt, err = chainWriter.ModifyAllocations(context.Background(), operatorAddr, allocateParams, waitForReceipt) require.NoError(t, err) require.Equal(t, gethtypes.ReceiptStatusSuccessful, receipt.Status) @@ -478,7 +671,7 @@ func TestAddAndRemovePendingAdmin(t *testing.T) { anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") require.NoError(t, err) contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) - // TODO: unhardcode permissionControllerAddr + permissionControllerAddr := common.HexToAddress(testutils.PERMISSION_CONTROLLER_ADDRESS) operatorAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) @@ -492,7 +685,7 @@ func TestAddAndRemovePendingAdmin(t *testing.T) { chainReader, err := testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) require.NoError(t, err) - pendingAdmin := common.HexToAddress("009440d62dc85c73dbf889b7ad1f4da8b231d2ef") + pendingAdmin := common.HexToAddress(testutils.ANVIL_THIRD_ADDRESS) request := elcontracts.AddPendingAdminRequest{ AccountAddress: operatorAddr, AdminAddress: pendingAdmin, @@ -504,6 +697,12 @@ func TestAddAndRemovePendingAdmin(t *testing.T) { AdminAddress: pendingAdmin, WaitForReceipt: true, } + + t.Run("remove pending admin when not added", func(t *testing.T) { + _, err := chainWriter.RemovePendingAdmin(context.Background(), removePendingAdminRequest) + require.Error(t, err, "cannot remove a pending admin that has not been added") + }) + t.Run("add pending admin", func(t *testing.T) { receipt, err := chainWriter.AddPendingAdmin(context.Background(), request) require.NoError(t, err) @@ -513,6 +712,12 @@ func TestAddAndRemovePendingAdmin(t *testing.T) { require.NoError(t, err) require.True(t, isPendingAdmin) }) + + t.Run("add pending admin when already added", func(t *testing.T) { + _, err := chainWriter.AddPendingAdmin(context.Background(), request) + require.Error(t, err, "cannot add a pending admin that has already been added") + }) + t.Run("remove pending admin", func(t *testing.T) { receipt, err := chainWriter.RemovePendingAdmin(context.Background(), removePendingAdminRequest) require.NoError(t, err) @@ -532,7 +737,6 @@ func TestAcceptAdmin(t *testing.T) { anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") require.NoError(t, err) contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) - // TODO: unhardcode permissionControllerAddr permissionControllerAddr := common.HexToAddress(testutils.PERMISSION_CONTROLLER_ADDRESS) accountAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) @@ -544,7 +748,7 @@ func TestAcceptAdmin(t *testing.T) { accountChainWriter, err := testclients.NewTestChainWriterFromConfig(anvilHttpEndpoint, accountPrivateKeyHex, config) require.NoError(t, err) - pendingAdminPrivateKeyHex := "4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356" + pendingAdminPrivateKeyHex := testutils.ANVIL_SECOND_PRIVATE_KEY adminChainWriter, err := testclients.NewTestChainWriterFromConfig( anvilHttpEndpoint, pendingAdminPrivateKeyHex, @@ -555,7 +759,7 @@ func TestAcceptAdmin(t *testing.T) { chainReader, err := testclients.NewTestChainReaderFromConfig(anvilHttpEndpoint, config) require.NoError(t, err) - pendingAdminAddr := common.HexToAddress("14dC79964da2C08b23698B3D3cc7Ca32193d9955") + pendingAdminAddr := common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS) request := elcontracts.AddPendingAdminRequest{ AccountAddress: accountAddr, AdminAddress: pendingAdminAddr, @@ -578,6 +782,11 @@ func TestAcceptAdmin(t *testing.T) { require.NoError(t, err) require.True(t, isAdmin) }) + + t.Run("accept admin when already accepted", func(t *testing.T) { + _, err = adminChainWriter.AcceptAdmin(context.Background(), acceptAdminRequest) + require.Error(t, err, "cannot accept an admin that has already been accepted") + }) } func TestRemoveAdmin(t *testing.T) { @@ -588,7 +797,7 @@ func TestRemoveAdmin(t *testing.T) { anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") require.NoError(t, err) contractAddrs := testutils.GetContractAddressesFromContractRegistry(anvilHttpEndpoint) - // TODO: unhardcode permissionControllerAddr + permissionControllerAddr := common.HexToAddress(testutils.PERMISSION_CONTROLLER_ADDRESS) accountAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) @@ -601,11 +810,11 @@ func TestRemoveAdmin(t *testing.T) { require.NoError(t, err) // Adding two admins and removing one. Cannot remove the last admin, so one must remain - admin1 := common.HexToAddress("14dC79964da2C08b23698B3D3cc7Ca32193d9955") - admin1PrivateKeyHex := "4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356" + admin1 := common.HexToAddress(testutils.ANVIL_SECOND_ADDRESS) + admin1PrivateKeyHex := testutils.ANVIL_SECOND_PRIVATE_KEY - admin2 := common.HexToAddress("23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f") - admin2PrivateKeyHex := "dbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97" + admin2 := common.HexToAddress(testutils.ANVIL_THIRD_ADDRESS) + admin2PrivateKeyHex := testutils.ANVIL_THIRD_PRIVATE_KEY admin1ChainWriter, err := testclients.NewTestChainWriterFromConfig(anvilHttpEndpoint, admin1PrivateKeyHex, config) require.NoError(t, err) @@ -630,6 +839,7 @@ func TestRemoveAdmin(t *testing.T) { AccountAddress: accountAddr, WaitForReceipt: true, } + // Add and accept admin 1 receipt, err := accountChainWriter.AddPendingAdmin(context.Background(), addAdmin1Request) require.NoError(t, err) @@ -653,6 +863,7 @@ func TestRemoveAdmin(t *testing.T) { AdminAddress: admin2, WaitForReceipt: true, } + t.Run("remove admin 2", func(t *testing.T) { receipt, err = admin1ChainWriter.RemoveAdmin(context.Background(), removeAdminRequest) require.NoError(t, err) @@ -662,157 +873,11 @@ func TestRemoveAdmin(t *testing.T) { require.NoError(t, err) require.False(t, isAdmin) }) -} - -// Returns a (test) claim for the given cumulativeEarnings, whose earner is -// the account given by the testutils.ANVIL_FIRST_ADDRESS address. -// This was taken from the eigensdk-rs -// https://github.com/Layr-Labs/eigensdk-rs/blob/d79b3672584b92f3c5fb204fde6bea394fbf0f12/crates/chainio/clients/elcontracts/src/lib.rs#L146 -func newTestClaim( - chainReader *elcontracts.ChainReader, - httpEndpoint string, - cumulativeEarnings int64, - privateKeyHex string, -) (*rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim, error) { - contractAddrs := testutils.GetContractAddressesFromContractRegistry(httpEndpoint) - mockStrategyAddr := contractAddrs.Erc20MockStrategy - rewardsCoordinatorAddr := contractAddrs.RewardsCoordinator - waitForReceipt := true - - ethClient, err := ethclient.Dial(httpEndpoint) - if err != nil { - return nil, utils.WrapError("Failed to create eth client", err) - } - - txManager, err := testclients.NewTestTxManager(httpEndpoint, privateKeyHex) - if err != nil { - return nil, utils.WrapError("Failed to create tx manager", err) - } - - contractStrategy, err := strategy.NewContractIStrategy(mockStrategyAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to fetch strategy contract", err) - } - - tokenAddr, err := contractStrategy.UnderlyingToken(&bind.CallOpts{Context: context.Background()}) - if err != nil { - return nil, utils.WrapError("Failed to fetch token address", err) - } - - token, err := mockerc20.NewContractMockERC20(tokenAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create token contract", err) - } - - noSendTxOpts, err := txManager.GetNoSendTxOpts() - if err != nil { - return nil, utils.WrapError("Failed to get NoSend tx opts", err) - } - - // Mint tokens for the RewardsCoordinator - tx, err := token.Mint(noSendTxOpts, rewardsCoordinatorAddr, big.NewInt(cumulativeEarnings)) - if err != nil { - return nil, utils.WrapError("Failed to create Mint tx", err) - } - - _, err = txManager.Send(context.Background(), tx, waitForReceipt) - if err != nil { - return nil, utils.WrapError("Failed to mint tokens for RewardsCoordinator", err) - } - - // Generate token tree leaf - // For the tree structure, see - // https://github.com/Layr-Labs/eigenlayer-contracts/blob/a888a1cd1479438dda4b138245a69177b125a973/docs/core/RewardsCoordinator.md#rewards-merkle-tree-structure - earnerAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) - tokenLeaf := rewardscoordinator.IRewardsCoordinatorTypesTokenTreeMerkleLeaf{ - Token: tokenAddr, - CumulativeEarnings: big.NewInt(cumulativeEarnings), - } - encodedTokenLeaf := []byte{} - tokenLeafSalt := uint8(1) - - // Write the *big.Int to a 32-byte sized buffer to match the uint256 length - cumulativeEarningsBytes := [32]byte{} - tokenLeaf.CumulativeEarnings.FillBytes(cumulativeEarningsBytes[:]) - - encodedTokenLeaf = append(encodedTokenLeaf, tokenLeafSalt) - encodedTokenLeaf = append(encodedTokenLeaf, tokenLeaf.Token.Bytes()...) - encodedTokenLeaf = append(encodedTokenLeaf, cumulativeEarningsBytes[:]...) - // Hash token tree leaf to get root - earnerTokenRoot := crypto.Keccak256(encodedTokenLeaf) - - // Generate earner tree leaf - earnerLeaf := rewardscoordinator.IRewardsCoordinatorTypesEarnerTreeMerkleLeaf{ - Earner: earnerAddr, - EarnerTokenRoot: [32]byte(earnerTokenRoot), - } - // Encode earner leaft - encodedEarnerLeaf := []byte{} - earnerLeafSalt := uint8(0) - encodedEarnerLeaf = append(encodedEarnerLeaf, earnerLeafSalt) - encodedEarnerLeaf = append(encodedEarnerLeaf, earnerLeaf.Earner.Bytes()...) - encodedEarnerLeaf = append(encodedEarnerLeaf, earnerTokenRoot...) - - // Hash encoded earner tree leaf to get root - earnerTreeRoot := crypto.Keccak256(encodedEarnerLeaf) - - // Fetch the next root index from contract - nextRootIndex, err := chainReader.GetDistributionRootsLength(context.Background()) - if err != nil { - return nil, utils.WrapError("Failed to call GetDistributionRootsLength", err) - } - - tokenLeaves := []rewardscoordinator.IRewardsCoordinatorTypesTokenTreeMerkleLeaf{tokenLeaf} - // Construct the claim - claim := rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim{ - RootIndex: uint32(nextRootIndex.Uint64()), - EarnerIndex: 0, - // Empty proof because leaf == root - EarnerTreeProof: []byte{}, - EarnerLeaf: earnerLeaf, - TokenIndices: []uint32{0}, - // Empty proof because leaf == root - TokenTreeProofs: [][]byte{{}}, - TokenLeaves: tokenLeaves, - } - - root := [32]byte(earnerTreeRoot) - // Fetch the current timestamp to increase it - currRewardsCalculationEndTimestamp, err := chainReader.CurrRewardsCalculationEndTimestamp(context.Background()) - if err != nil { - return nil, utils.WrapError("Failed to call CurrRewardsCalculationEndTimestamp", err) - } - - rewardsCoordinator, err := rewardscoordinator.NewContractIRewardsCoordinator(rewardsCoordinatorAddr, ethClient) - if err != nil { - return nil, utils.WrapError("Failed to create rewards coordinator contract", err) - } - - rewardsUpdater := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) - - // Change the rewards updater to be able to submit the new root - tx, err = rewardsCoordinator.SetRewardsUpdater(noSendTxOpts, rewardsUpdater) - if err != nil { - return nil, utils.WrapError("Failed to create SetRewardsUpdater tx", err) - } - - _, err = txManager.Send(context.Background(), tx, waitForReceipt) - if err != nil { - return nil, utils.WrapError("Failed to setRewardsUpdate", err) - } - - tx, err = rewardsCoordinator.SubmitRoot(noSendTxOpts, root, currRewardsCalculationEndTimestamp+1) - if err != nil { - return nil, utils.WrapError("Failed to create SubmitRoot tx", err) - } - - _, err = txManager.Send(context.Background(), tx, waitForReceipt) - if err != nil { - return nil, utils.WrapError("Failed to submit root", err) - } - - return &claim, nil + t.Run("remove admin 2 when already removed", func(t *testing.T) { + _, err := admin1ChainWriter.RemoveAdmin(context.Background(), removeAdminRequest) + require.Error(t, err, "cannot remove an admin that has already been removed") + }) } func TestProcessClaim(t *testing.T) { @@ -890,12 +955,17 @@ func TestProcessClaims(t *testing.T) { cumulativeEarnings1 := int64(42) cumulativeEarnings2 := int64(4256) + emptyClaims := []rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim{} + _, err = chainWriter.ProcessClaims(context.Background(), emptyClaims, recipient, waitForReceipt) + require.Error(t, err, "cannot process empty claims") + // Generate 2 claims claim1, err := newTestClaim(chainReader, anvilHttpEndpoint, cumulativeEarnings1, privateKeyHex) require.NoError(t, err) claim2, err := newTestClaim(chainReader, anvilHttpEndpoint, cumulativeEarnings2, privateKeyHex) require.NoError(t, err) + claims := []rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim{ *claim1, *claim2, } @@ -1052,3 +1122,154 @@ func setTestRewardsCoordinatorActivationDelay( } return receipt, err } + +// Returns a (test) claim for the given cumulativeEarnings, whose earner is +// the account given by the testutils.ANVIL_FIRST_ADDRESS address. +// This was taken from the eigensdk-rs +// https://github.com/Layr-Labs/eigensdk-rs/blob/d79b3672584b92f3c5fb204fde6bea394fbf0f12/crates/chainio/clients/elcontracts/src/lib.rs#L146 +func newTestClaim( + chainReader *elcontracts.ChainReader, + httpEndpoint string, + cumulativeEarnings int64, + privateKeyHex string, +) (*rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim, error) { + contractAddrs := testutils.GetContractAddressesFromContractRegistry(httpEndpoint) + mockStrategyAddr := contractAddrs.Erc20MockStrategy + rewardsCoordinatorAddr := contractAddrs.RewardsCoordinator + waitForReceipt := true + + ethClient, err := ethclient.Dial(httpEndpoint) + if err != nil { + return nil, utils.WrapError("Failed to create eth client", err) + } + + txManager, err := testclients.NewTestTxManager(httpEndpoint, privateKeyHex) + if err != nil { + return nil, utils.WrapError("Failed to create tx manager", err) + } + + contractStrategy, err := strategy.NewContractIStrategy(mockStrategyAddr, ethClient) + if err != nil { + return nil, utils.WrapError("Failed to fetch strategy contract", err) + } + + tokenAddr, err := contractStrategy.UnderlyingToken(&bind.CallOpts{Context: context.Background()}) + if err != nil { + return nil, utils.WrapError("Failed to fetch token address", err) + } + + token, err := mockerc20.NewContractMockERC20(tokenAddr, ethClient) + if err != nil { + return nil, utils.WrapError("Failed to create token contract", err) + } + + noSendTxOpts, err := txManager.GetNoSendTxOpts() + if err != nil { + return nil, utils.WrapError("Failed to get NoSend tx opts", err) + } + + // Mint tokens for the RewardsCoordinator + tx, err := token.Mint(noSendTxOpts, rewardsCoordinatorAddr, big.NewInt(cumulativeEarnings)) + if err != nil { + return nil, utils.WrapError("Failed to create Mint tx", err) + } + + _, err = txManager.Send(context.Background(), tx, waitForReceipt) + if err != nil { + return nil, utils.WrapError("Failed to mint tokens for RewardsCoordinator", err) + } + + // Generate token tree leaf + // For the tree structure, see + // https://github.com/Layr-Labs/eigenlayer-contracts/blob/a888a1cd1479438dda4b138245a69177b125a973/docs/core/RewardsCoordinator.md#rewards-merkle-tree-structure + earnerAddr := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) + tokenLeaf := rewardscoordinator.IRewardsCoordinatorTypesTokenTreeMerkleLeaf{ + Token: tokenAddr, + CumulativeEarnings: big.NewInt(cumulativeEarnings), + } + encodedTokenLeaf := []byte{} + tokenLeafSalt := uint8(1) + + // Write the *big.Int to a 32-byte sized buffer to match the uint256 length + cumulativeEarningsBytes := [32]byte{} + tokenLeaf.CumulativeEarnings.FillBytes(cumulativeEarningsBytes[:]) + + encodedTokenLeaf = append(encodedTokenLeaf, tokenLeafSalt) + encodedTokenLeaf = append(encodedTokenLeaf, tokenLeaf.Token.Bytes()...) + encodedTokenLeaf = append(encodedTokenLeaf, cumulativeEarningsBytes[:]...) + + // Hash token tree leaf to get root + earnerTokenRoot := crypto.Keccak256(encodedTokenLeaf) + + // Generate earner tree leaf + earnerLeaf := rewardscoordinator.IRewardsCoordinatorTypesEarnerTreeMerkleLeaf{ + Earner: earnerAddr, + EarnerTokenRoot: [32]byte(earnerTokenRoot), + } + // Encode earner leaft + encodedEarnerLeaf := []byte{} + earnerLeafSalt := uint8(0) + encodedEarnerLeaf = append(encodedEarnerLeaf, earnerLeafSalt) + encodedEarnerLeaf = append(encodedEarnerLeaf, earnerLeaf.Earner.Bytes()...) + encodedEarnerLeaf = append(encodedEarnerLeaf, earnerTokenRoot...) + + // Hash encoded earner tree leaf to get root + earnerTreeRoot := crypto.Keccak256(encodedEarnerLeaf) + + // Fetch the next root index from contract + nextRootIndex, err := chainReader.GetDistributionRootsLength(context.Background()) + if err != nil { + return nil, utils.WrapError("Failed to call GetDistributionRootsLength", err) + } + + tokenLeaves := []rewardscoordinator.IRewardsCoordinatorTypesTokenTreeMerkleLeaf{tokenLeaf} + // Construct the claim + claim := rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim{ + RootIndex: uint32(nextRootIndex.Uint64()), + EarnerIndex: 0, + // Empty proof because leaf == root + EarnerTreeProof: []byte{}, + EarnerLeaf: earnerLeaf, + TokenIndices: []uint32{0}, + // Empty proof because leaf == root + TokenTreeProofs: [][]byte{{}}, + TokenLeaves: tokenLeaves, + } + + root := [32]byte(earnerTreeRoot) + // Fetch the current timestamp to increase it + currRewardsCalculationEndTimestamp, err := chainReader.CurrRewardsCalculationEndTimestamp(context.Background()) + if err != nil { + return nil, utils.WrapError("Failed to call CurrRewardsCalculationEndTimestamp", err) + } + + rewardsCoordinator, err := rewardscoordinator.NewContractIRewardsCoordinator(rewardsCoordinatorAddr, ethClient) + if err != nil { + return nil, utils.WrapError("Failed to create rewards coordinator contract", err) + } + + rewardsUpdater := common.HexToAddress(testutils.ANVIL_FIRST_ADDRESS) + + // Change the rewards updater to be able to submit the new root + tx, err = rewardsCoordinator.SetRewardsUpdater(noSendTxOpts, rewardsUpdater) + if err != nil { + return nil, utils.WrapError("Failed to create SetRewardsUpdater tx", err) + } + + _, err = txManager.Send(context.Background(), tx, waitForReceipt) + if err != nil { + return nil, utils.WrapError("Failed to setRewardsUpdate", err) + } + + tx, err = rewardsCoordinator.SubmitRoot(noSendTxOpts, root, currRewardsCalculationEndTimestamp+1) + if err != nil { + return nil, utils.WrapError("Failed to create SubmitRoot tx", err) + } + + _, err = txManager.Send(context.Background(), tx, waitForReceipt) + if err != nil { + return nil, utils.WrapError("Failed to submit root", err) + } + + return &claim, nil +} diff --git a/chainio/txmgr/geometric/geometric_example_test.go b/chainio/txmgr/geometric/geometric_example_test.go index e6820be2..54886834 100644 --- a/chainio/txmgr/geometric/geometric_example_test.go +++ b/chainio/txmgr/geometric/geometric_example_test.go @@ -12,6 +12,7 @@ import ( "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/signerv2" "github.com/Layr-Labs/eigensdk-go/testutils" + "github.com/Layr-Labs/eigensdk-go/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -23,63 +24,76 @@ var ( chainid = big.NewInt(31337) ) +func createTx(client eth.HttpBackend, address common.Address) (*types.Transaction, error) { + zeroAddr := common.HexToAddress("0x0") + nonce, err := client.PendingNonceAt(context.TODO(), address) + if err != nil { + return nil, utils.WrapError("Failed to get PendingNonceAt", err) + } + return types.NewTx(&types.DynamicFeeTx{ + To: &zeroAddr, + Nonce: nonce, + }), nil +} + +func createTxMgr(rpcUrl string, ecdsaPrivateKey *ecdsa.PrivateKey) (eth.HttpBackend, *GeometricTxManager, error) { + logger := logging.NewTextSLogger(os.Stdout, &logging.SLoggerOptions{}) + client, err := ethclient.Dial(rpcUrl) + if err != nil { + return nil, nil, err + } + signerV2, signerAddr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid) + if err != nil { + return nil, nil, err + } + wallet, err := wallet.NewPrivateKeyWallet(client, signerV2, signerAddr, logger) + if err != nil { + return nil, nil, err + } + reg := prometheus.NewRegistry() + metrics := NewMetrics(reg, "example", logger) + return client, NewGeometricTxnManager(client, wallet, logger, metrics, GeometricTxnManagerParams{}), nil +} + func ExampleGeometricTxManager() { anvilC, err := testutils.StartAnvilContainer("") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } anvilUrl, err := anvilC.Endpoint(context.TODO(), "http") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } ecdsaPrivateKey, err := crypto.HexToECDSA(testutils.ANVIL_FIRST_PRIVATE_KEY) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } pk := ecdsaPrivateKey.PublicKey address := crypto.PubkeyToAddress(pk) - client, txmgr := createTxMgr(anvilUrl, ecdsaPrivateKey) - - tx := createTx(client, address) - _, err = txmgr.Send(context.TODO(), tx, true) + client, txmgr, err := createTxMgr(anvilUrl, ecdsaPrivateKey) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - // we just add this to make sure the example runs - fmt.Println("Tx sent") - // Output: Tx sent -} - -func createTx(client eth.HttpBackend, address common.Address) *types.Transaction { - zeroAddr := common.HexToAddress("0x0") - nonce, err := client.PendingNonceAt(context.TODO(), address) + tx, err := createTx(client, address) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - return types.NewTx(&types.DynamicFeeTx{ - To: &zeroAddr, - Nonce: nonce, - }) -} -func createTxMgr(rpcUrl string, ecdsaPrivateKey *ecdsa.PrivateKey) (eth.HttpBackend, *GeometricTxManager) { - logger := logging.NewTextSLogger(os.Stdout, &logging.SLoggerOptions{}) - client, err := ethclient.Dial(rpcUrl) - if err != nil { - panic(err) - } - signerV2, signerAddr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid) - if err != nil { - panic(err) - } - wallet, err := wallet.NewPrivateKeyWallet(client, signerV2, signerAddr, logger) + _, err = txmgr.Send(context.TODO(), tx, true) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - reg := prometheus.NewRegistry() - metrics := NewMetrics(reg, "example", logger) - return client, NewGeometricTxnManager(client, wallet, logger, metrics, GeometricTxnManagerParams{}) + + // we just add this to make sure the example runs + fmt.Println("Tx sent") + // Output: Tx sent } diff --git a/chainio/txmgr/simple.go b/chainio/txmgr/simple.go index 6c732ad2..cd77c3bf 100644 --- a/chainio/txmgr/simple.go +++ b/chainio/txmgr/simple.go @@ -8,6 +8,8 @@ import ( "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" "github.com/Layr-Labs/eigensdk-go/logging" + "github.com/Layr-Labs/eigensdk-go/utils" + "github.com/cenkalti/backoff/v4" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -91,6 +93,38 @@ func (m *SimpleTxManager) Send( return receipt, nil } +// SendWithRetry is used to send a transaction to the Ethereum node, same as Send but adding retry logic. +// If the transaction fails, it will retry sending the transaction until it gets a receipt, using +// **exponential backoff** with factor `multiplier`, starting with `initialInterval`. +func (m *SimpleTxManager) SendWithRetry( + ctx context.Context, + tx *types.Transaction, + initialInterval time.Duration, + maxElapsedTime time.Duration, + multiplier float64, +) (*types.Receipt, error) { + backoffConfig := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(initialInterval), + backoff.WithMultiplier(multiplier), + backoff.WithMaxElapsedTime(maxElapsedTime), + ) + + retryCount := 0 + + sendAndWait := func() (*types.Receipt, error) { + defer func() { retryCount++ }() + + r, err := m.send(ctx, tx) + if err != nil { + m.logger.Warn("failed to send transaction", err, "retryCount", retryCount) + return nil, err + } + return m.waitForReceipt(ctx, r.TxHash.Hex()) + } + + return backoff.RetryWithData(sendAndWait, backoffConfig) +} + func (m *SimpleTxManager) send(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { // Estimate gas and nonce // can't print tx hash in logs because the tx changes below when we complete and sign it @@ -139,7 +173,7 @@ func (m *SimpleTxManager) waitForReceipt(ctx context.Context, txID wallet.TxID) for { select { case <-ctx.Done(): - return nil, errors.Join(errors.New("Context done before tx was mined"), ctx.Err()) + return nil, utils.WrapError(ctx.Err(), "context done before tx was mined") case <-queryTicker.C: if receipt := m.queryReceipt(ctx, txID); receipt != nil { return receipt, nil diff --git a/chainio/txmgr/simple_test.go b/chainio/txmgr/simple_test.go new file mode 100644 index 00000000..5a785757 --- /dev/null +++ b/chainio/txmgr/simple_test.go @@ -0,0 +1,130 @@ +package txmgr_test + +import ( + "context" + "fmt" + "math/big" + "testing" + "time" + + "github.com/Layr-Labs/eigensdk-go/chainio/clients/wallet" + "github.com/Layr-Labs/eigensdk-go/chainio/txmgr" + "github.com/Layr-Labs/eigensdk-go/signerv2" + "github.com/Layr-Labs/eigensdk-go/testutils" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newTx() *types.Transaction { + return types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(31337), + Nonce: 0, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(1_000_000_000), + Gas: 21000, + To: testutils.ZeroAddress(), + Value: big.NewInt(1), + }) +} + +func TestSendWithRetryWithNoError(t *testing.T) { + // Test SendWithRetry with a non-failing transaction to verify normal behavior + ecdsaPrivateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + anvilC, err := testutils.StartAnvilContainer("") + require.NoError(t, err) + anvilHttpEndpoint, err := anvilC.Endpoint(context.Background(), "http") + require.NoError(t, err) + logger := testutils.NewTestLogger() + + ethHttpClient, err := ethclient.Dial(anvilHttpEndpoint) + require.NoError(t, err) + + chainid, err := ethHttpClient.ChainID(context.Background()) + require.NoError(t, err) + + signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaPrivateKey}, chainid) + require.NoError(t, err) + + pkWallet, err := wallet.NewPrivateKeyWallet(ethHttpClient, signerV2, addr, logger) + require.NoError(t, err) + + txMgr := txmgr.NewSimpleTxManager(pkWallet, ethHttpClient, logger, addr) + + tx := newTx() + retryTimeout := 200 * time.Millisecond + maxElapsedTime := 2 * time.Second + multiplier := 1.5 + + _, err = txMgr.SendWithRetry(context.Background(), tx, retryTimeout, maxElapsedTime, multiplier) + require.NoError(t, err) +} + +func TestSendWithRetryDoesBackoff(t *testing.T) { + // Test SendWithRetry using a FailingEthBackend to simulate errors when sending transactions + logger := testutils.NewTestLogger() + ethBackend := NewFailingEthBackend(3) + + chainid := big.NewInt(31337) + ecdsaSk, _, err := testutils.NewEcdsaSkAndAddress() + require.NoError(t, err) + + signerV2, addr, err := signerv2.SignerFromConfig(signerv2.Config{PrivateKey: ecdsaSk}, chainid) + require.NoError(t, err) + + pkWallet, err := wallet.NewPrivateKeyWallet(ethBackend, signerV2, addr, logger) + require.NoError(t, err) + + txMgr := txmgr.NewSimpleTxManager(pkWallet, ethBackend, logger, addr) + + tx := newTx() + retryTimeout := 200 * time.Millisecond + maxElapsedTime := 3 * time.Second + multiplier := 1.5 + + _, err = txMgr.SendWithRetry(context.Background(), tx, retryTimeout, maxElapsedTime, multiplier) + require.NoError(t, err) + assert.Equal(t, ethBackend.pendingFailures, uint32(0)) +} + +// Mock of the EthBackend that returns an error when sending transactions. +// Once pendingFailures reaches zero, SendTransaction will no longer fail +type FailingEthBackend struct { + pendingFailures uint32 +} + +func NewFailingEthBackend(pendingFailures uint32) *FailingEthBackend { + backend := &FailingEthBackend{pendingFailures: pendingFailures} + return backend +} + +func (s *FailingEthBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error { + if s.pendingFailures == 0 { + return nil + } + s.pendingFailures-- + return fmt.Errorf("did not send tx") +} + +func (s *FailingEthBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + return &types.Receipt{}, nil +} + +func (s *FailingEthBackend) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + return 0, nil +} + +func (s *FailingEthBackend) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + return &types.Header{ + BaseFee: big.NewInt(0), + }, nil +} + +func (s *FailingEthBackend) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} diff --git a/contracts/script/DeployMockAvs.s.sol b/contracts/script/DeployMockAvs.s.sol index c8b4f952..892f8f9d 100644 --- a/contracts/script/DeployMockAvs.s.sol +++ b/contracts/script/DeployMockAvs.s.sol @@ -87,4 +87,4 @@ contract DeployMockAvs is DeployMockAvsRegistries { } vm.stopBroadcast(); } -} \ No newline at end of file +} diff --git a/contracts/script/DeployMockAvsRegistries.s.sol b/contracts/script/DeployMockAvsRegistries.s.sol index d4cb714c..9a01626f 100644 --- a/contracts/script/DeployMockAvsRegistries.s.sol +++ b/contracts/script/DeployMockAvsRegistries.s.sol @@ -211,4 +211,4 @@ contract DeployMockAvsRegistries is Script, ConfigsReadWriter, EigenlayerContrac registry.registerContract("strategyManager", address(eigen.strategyManager)); registry.registerContract("rewardsCoordinator", address(eigen.rewardsCoordinator)); } -} \ No newline at end of file +} diff --git a/contracts/script/output/31337/mockAvs_deployment_output.json b/contracts/script/output/31337/mockAvs_deployment_output.json index fe721fbb..33ae3f2c 100644 --- a/contracts/script/output/31337/mockAvs_deployment_output.json +++ b/contracts/script/output/31337/mockAvs_deployment_output.json @@ -7,4 +7,4 @@ "registryCoordinator": "0x95401dc811bb5740090279Ba06cfA8fcF6113778", "registryCoordinatorImplementation": "0x4c5859f0F772848b2D91F1D83E2Fe57935348029" } -} \ No newline at end of file +} diff --git a/contracts/script/output/31337/token_and_strategy_deployment_output.json b/contracts/script/output/31337/token_and_strategy_deployment_output.json index e0eb563b..1596c2ce 100644 --- a/contracts/script/output/31337/token_and_strategy_deployment_output.json +++ b/contracts/script/output/31337/token_and_strategy_deployment_output.json @@ -3,4 +3,4 @@ "erc20mock": "0xFD471836031dc5108809D173A067e8486B9047A3", "erc20mockstrategy": "0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f" } -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 8217b155..dc8369b9 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/containerd/containerd v1.7.12 // indirect diff --git a/go.sum b/go.sum index 9584993b..0cd10ba8 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPx github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= diff --git a/logging/slog_logger.go b/logging/slog_logger.go index 43490a1f..30d91514 100644 --- a/logging/slog_logger.go +++ b/logging/slog_logger.go @@ -95,40 +95,6 @@ func NewJsonSLogger(outputWriter io.Writer, opts *SLoggerOptions) *SLogger { } } -// NewSlogTextLogger creates a new SLogger with a text handler -// -// outputWriter is the writer to write the logs to (typically os.Stdout, -// but can also use a io.MultiWriter(os.Stdout, file) to write to multiple outputs) -// handlerOptions is the options for the handler, such as -// Level is the minimum level to log -// AddSource if true, adds source information to the log -// -// Deprecated: use NewTextSLogger instead -func NewSlogTextLogger(outputWriter io.Writer, handlerOpts *slog.HandlerOptions) *SLogger { - handler := slog.NewTextHandler(outputWriter, handlerOpts) - logger := slog.New(handler) - return &SLogger{ - logger, - } -} - -// NewSlogJsonLogger creates a new SLogger with a Json handler -// -// outputWriter is the writer to write the logs to (typically os.Stdout, -// but can also use a io.MultiWriter(os.Stdout, file) to write to multiple outputs) -// handlerOptions is the options for the handler, such as -// Level is the minimum level to log -// AddSource if true, adds source information to the log -// -// Deprecated: use NewJsonSLogger instead -func NewSlogJsonLogger(outputWriter io.Writer, handlerOpts *slog.HandlerOptions) *SLogger { - handler := slog.NewJSONHandler(outputWriter, handlerOpts) - logger := slog.New(handler) - return &SLogger{ - logger, - } -} - func (s SLogger) Debug(msg string, tags ...any) { s.logCorrectSource(slog.LevelDebug, msg, tags...) } diff --git a/logging/zap_logger.go b/logging/zap_logger.go index 5120a5ca..38d31ffe 100644 --- a/logging/zap_logger.go +++ b/logging/zap_logger.go @@ -3,6 +3,7 @@ package logging import ( "fmt" + "github.com/Layr-Labs/eigensdk-go/utils" "go.uber.org/zap" ) @@ -28,7 +29,7 @@ func NewZapLogger(env LogLevel) (Logger, error) { } else if env == Development { config = zap.NewDevelopmentConfig() } else { - panic(fmt.Sprintf("Unknown environment. Expected %s or %s. Received %s.", Development, Production, env)) + return nil, fmt.Errorf("unknown environment. Expected %s or %s. Received %s", Development, Production, env) } return NewZapLoggerByConfig(config, zap.AddCallerSkip(1)) @@ -39,7 +40,7 @@ func NewZapLogger(env LogLevel) (Logger, error) { func NewZapLoggerByConfig(config zap.Config, options ...zap.Option) (Logger, error) { logger, err := config.Build(options...) if err != nil { - panic(err) + return nil, utils.WrapError("Can not build config with the given options", err) } return &ZapLogger{ diff --git a/metrics/eigenmetrics_example_test.go b/metrics/eigenmetrics_example_test.go index bf55ab83..f4075d89 100644 --- a/metrics/eigenmetrics_example_test.go +++ b/metrics/eigenmetrics_example_test.go @@ -6,6 +6,8 @@ package metrics_test import ( "context" + "fmt" + "os" "github.com/Layr-Labs/eigensdk-go/chainio/clients" "github.com/Layr-Labs/eigensdk-go/chainio/clients/eth" @@ -26,13 +28,15 @@ func ExampleEigenMetrics() { logger, err := logging.NewZapLogger("development") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } // get the Writer for the EL contracts ecdsaPrivateKey, err := crypto.HexToECDSA("0x0") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } operatorEcdsaAddr := crypto.PubkeyToAddress(ecdsaPrivateKey.PublicKey) @@ -46,7 +50,8 @@ func ExampleEigenMetrics() { } clients, err := clients.BuildAll(chainioConfig, ecdsaPrivateKey, logger) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } reg := prometheus.NewRegistry() eigenMetrics := metrics.NewEigenMetrics("exampleAvs", ":9090", reg, logger) @@ -71,7 +76,8 @@ func ExampleEigenMetrics() { rpcCallsCollector := rpccalls.NewCollector("exampleAvs", reg) instrumentedEthClient, err := eth.NewInstrumentedClient("http://localhost:8545", rpcCallsCollector) if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } eigenMetrics.Start(context.Background(), reg) diff --git a/nodeapi/nodeapi_example_test.go b/nodeapi/nodeapi_example_test.go index 7c9e7844..2fa76047 100644 --- a/nodeapi/nodeapi_example_test.go +++ b/nodeapi/nodeapi_example_test.go @@ -1,6 +1,9 @@ package nodeapi_test import ( + "fmt" + "os" + "github.com/Layr-Labs/eigensdk-go/logging" "github.com/Layr-Labs/eigensdk-go/nodeapi" ) @@ -8,7 +11,8 @@ import ( func ExampleNodeApi() { logger, err := logging.NewZapLogger("development") if err != nil { - panic(err) + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } nodeApi := nodeapi.NewNodeApi("testAvs", "v0.0.1", "localhost:8080", logger) diff --git a/services/avsregistry/avsregistry_chaincaller.go b/services/avsregistry/avsregistry_chaincaller.go index 5b2da475..b7966ffd 100644 --- a/services/avsregistry/avsregistry_chaincaller.go +++ b/services/avsregistry/avsregistry_chaincaller.go @@ -76,11 +76,20 @@ func (ar *AvsRegistryServiceChainCaller) GetOperatorsAvsStateAtBlock( } numquorums := len(quorumNumbers) if len(operatorsStakesInQuorums) != numquorums { - ar.logger.Fatal( + ar.logger.Error( "Number of quorums returned from GetOperatorsStakeInQuorumsAtBlock does not match number of quorums requested. Probably pointing to old contract or wrong implementation.", "service", "AvsRegistryServiceChainCaller", + "operatorsStakesInQuorums", + operatorsStakesInQuorums, + "numquorums", + numquorums, ) + return nil, + utils.WrapError( + "number of quorums returned from GetOperatorsStakeInQuorumsAtBlock does not match number of quorums requested. Probably pointing to old contract or wrong implementation", + nil, + ) } for quorumIdx, quorumNum := range quorumNumbers { @@ -149,7 +158,7 @@ func (ar *AvsRegistryServiceChainCaller) getOperatorInfo( info, ok := ar.operatorInfoService.GetOperatorInfo(ctx, operatorAddr) if !ok { return types.OperatorInfo{}, fmt.Errorf( - "Failed to get operator info from operatorInfoService (operatorAddr: %v, operatorId: %v)", + "failed to get operator info from operatorInfoService (operatorAddr: %v, operatorId: %v)", operatorAddr, operatorId, ) diff --git a/services/avsregistry/avsregistry_fake.go b/services/avsregistry/avsregistry_fake.go index d5cf8e59..db2a25ad 100644 --- a/services/avsregistry/avsregistry_fake.go +++ b/services/avsregistry/avsregistry_fake.go @@ -29,7 +29,7 @@ func NewFakeAvsRegistryService(blockNum types.BlockNum, operators []types.TestOp G1Pubkey: operator.BlsKeypair.GetPubKeyG1(), G2Pubkey: operator.BlsKeypair.GetPubKeyG2(), }, - Socket: "localhost:8080", + Socket: operator.Socket, }, StakePerQuorum: operator.StakePerQuorum, BlockNumber: blockNum, diff --git a/services/bls_aggregation/blsagg_test.go b/services/bls_aggregation/blsagg_test.go index 5e3d0425..4e41357c 100644 --- a/services/bls_aggregation/blsagg_test.go +++ b/services/bls_aggregation/blsagg_test.go @@ -117,16 +117,19 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(300), 1: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -207,11 +210,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -282,11 +287,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } quorumNumbers := types.QuorumNums{0, 1} quorumThresholdPercentages := []types.QuorumThresholdPercentage{100, 100} @@ -418,11 +425,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } blockNum := uint32(1) @@ -513,6 +522,7 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0} @@ -543,11 +553,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0} @@ -602,11 +614,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -653,12 +667,14 @@ func TestBlsAgg(t *testing.T) { // Note the quorums is {0, 1}, but operator id 1 just stake 0. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, // Note the quorums is {0, 1}, but operator id 1 just stake 0. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0, 1} @@ -728,17 +744,20 @@ func TestBlsAgg(t *testing.T) { // Note the quorums is {0, 1}, but operator id 1 just stake 0. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, // Note the quorums is {0, 1}, but operator id 2 just stake 1. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0, 1} @@ -815,17 +834,20 @@ func TestBlsAgg(t *testing.T) { // Note the quorums is {0, 1}, but operator id 1 just stake 0. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, // Note the quorums is {0, 1}, but operator id 2 just stake 1. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0, 1} @@ -931,11 +953,13 @@ func TestBlsAgg(t *testing.T) { // Note the quorums is {0, 1}, but operator id 1 just stake 0. StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } taskIndex := types.TaskIndex(0) quorumNumbers := types.QuorumNums{0, 1} @@ -1015,11 +1039,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -1086,11 +1112,13 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -1194,16 +1222,19 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -1299,16 +1330,19 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) @@ -1506,16 +1540,19 @@ func TestBlsAgg(t *testing.T) { OperatorId: types.OperatorId{1}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x1"), + Socket: "localhost:8080", } testOperator2 := types.TestOperator{ OperatorId: types.OperatorId{2}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(200)}, BlsKeypair: newBlsKeyPairPanics("0x2"), + Socket: "localhost:8081", } testOperator3 := types.TestOperator{ OperatorId: types.OperatorId{3}, StakePerQuorum: map[types.QuorumNum]types.StakeAmount{0: big.NewInt(100), 1: big.NewInt(100)}, BlsKeypair: newBlsKeyPairPanics("0x3"), + Socket: "localhost:8082", } blockNum := uint32(1) taskIndex := types.TaskIndex(0) diff --git a/services/operatorsinfo/operatorsinfo_inmemory.go b/services/operatorsinfo/operatorsinfo_inmemory.go index c2874b92..cf3d6b24 100644 --- a/services/operatorsinfo/operatorsinfo_inmemory.go +++ b/services/operatorsinfo/operatorsinfo_inmemory.go @@ -9,6 +9,7 @@ import ( blsapkreg "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" "github.com/ethereum/go-ethereum/event" + "golang.org/x/sync/errgroup" "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/Layr-Labs/eigensdk-go/logging" @@ -49,17 +50,13 @@ type avsRegistrySubscriber interface { // Warning: this service should probably not be used in production. Haven't done a thorough analysis of all the clients // but there is still an open PR about an issue with ws subscription on geth: // https://github.com/ethereum/go-ethereum/issues/23845 -// Another reason to note for infra/devops engineer who would put this into production, is that this service crashes on -// websocket connection errors or when failing to query past events. The philosophy here is that hard crashing is -// better than silently failing, since it will be easier to debug. Naturally, this means that this aggregator using this -// service needs -// to be replicated and load-balanced, so that when it fails traffic can be switched to the other aggregator. type OperatorsInfoServiceInMemory struct { logFilterQueryBlockRange *big.Int avsRegistrySubscriber avsRegistrySubscriber avsRegistryReader avsRegistryReader logger logging.Logger queryC chan<- query + errG *errgroup.Group // queried via the queryC channel, so don't need mutex to access pubkeyDict map[common.Address]types.OperatorPubkeys operatorAddrToId map[common.Address]types.OperatorId @@ -104,12 +101,15 @@ func NewOperatorsInfoServiceInMemory( if logFilterQueryBlockRange == nil { logFilterQueryBlockRange = defaultLogFilterQueryBlockRange } + errG, ctx := errgroup.WithContext(ctx) + pkcs := &OperatorsInfoServiceInMemory{ avsRegistrySubscriber: avsRegistrySubscriber, avsRegistryReader: avsRegistryReader, logFilterQueryBlockRange: logFilterQueryBlockRange, logger: logger, queryC: queryC, + errG: errG, pubkeyDict: make(map[common.Address]types.OperatorPubkeys), operatorAddrToId: make(map[common.Address]types.OperatorId), socketDict: make(map[types.OperatorId]types.Socket), @@ -117,169 +117,167 @@ func NewOperatorsInfoServiceInMemory( // We use this waitgroup to wait on the initialization of the inmemory pubkey dict, // which requires querying the past events of the pubkey registration contract wg := sync.WaitGroup{} + wg.Add(1) - pkcs.startServiceInGoroutine(ctx, queryC, &wg, opts) + errG.Go(func() error { + return pkcs.runService(ctx, queryC, &wg, opts) + }) wg.Wait() return pkcs } -func (ops *OperatorsInfoServiceInMemory) startServiceInGoroutine( +func (ops *OperatorsInfoServiceInMemory) runService( ctx context.Context, queryC <-chan query, wg *sync.WaitGroup, opts Opts, -) { - go func() { - - // TODO(samlaf): we should probably save the service in the logger itself and add it automatically to all logs - ops.logger.Debug( - "Subscribing to new pubkey registration events on blsApkRegistry contract", +) error { + // TODO(samlaf): we should probably save the service in the logger itself and add it automatically to all logs + ops.logger.Debug( + "Subscribing to new pubkey registration events on blsApkRegistry contract", + "service", + "OperatorPubkeysServiceInMemory", + ) + newPubkeyRegistrationC, newPubkeyRegistrationSub, err := ops.avsRegistrySubscriber.SubscribeToNewPubkeyRegistrations() + if err != nil { + ops.logger.Error( + "Fatal error opening websocket subscription for new pubkey registrations", + "err", + err, "service", "OperatorPubkeysServiceInMemory", ) - newPubkeyRegistrationC, newPubkeyRegistrationSub, err := ops.avsRegistrySubscriber.SubscribeToNewPubkeyRegistrations() - if err != nil { - ops.logger.Error( - "Fatal error opening websocket subscription for new pubkey registrations", - "err", - err, - "service", - "OperatorPubkeysServiceInMemory", - ) - // see the warning above the struct definition to understand why we panic here - panic(err) - } - newSocketRegistrationC, newSocketRegistrationSub, err := ops.avsRegistrySubscriber.SubscribeToOperatorSocketUpdates() - if err != nil { - ops.logger.Error( - "Fatal error opening websocket subscription for new socket registrations", - "err", - err, - "service", - "OperatorPubkeysServiceInMemory", - ) - panic(err) - } - err = ops.queryPastRegisteredOperatorEventsAndFillDb(ctx, opts) - if err != nil { + return err + } + newSocketRegistrationC, newSocketRegistrationSub, err := ops.avsRegistrySubscriber.SubscribeToOperatorSocketUpdates() + if err != nil { + ops.logger.Error( + "Fatal error opening websocket subscription for new socket registrations", + "err", + err, + "service", + "OperatorPubkeysServiceInMemory", + ) + return err + } + err = ops.queryPastRegisteredOperatorEventsAndFillDb(ctx, opts) + if err != nil { + ops.logger.Error( + "Fatal error querying past registered operator events and filling db", + "err", + err, + "service", + "OperatorPubkeysServiceInMemory", + ) + return err + } + // The constructor can return after we have backfilled the db by querying the events of operators that have + // registered with the blsApkRegistry + // before the block at which we started the ws subscription above + wg.Done() + for { + select { + case <-ctx.Done(): + // TODO(samlaf): should we do anything here? Seems like this only happens when the aggregator is + // shutting down and we want graceful exit + ops.logger.Infof("OperatorPubkeysServiceInMemory: Context cancelled, exiting") + return errors.New("OperatorPubkeysServiceInMemory: Context cancelled, exiting") + case err := <-newPubkeyRegistrationSub.Err(): ops.logger.Error( - "Fatal error querying past registered operator events and filling db", + "Error in websocket subscription for new pubkey registration events. Attempting to reconnect...", "err", err, "service", "OperatorPubkeysServiceInMemory", ) - panic(err) - } - // The constructor can return after we have backfilled the db by querying the events of operators that have - // registered with the blsApkRegistry - // before the block at which we started the ws subscription above - wg.Done() - for { - select { - case <-ctx.Done(): - // TODO(samlaf): should we do anything here? Seems like this only happens when the aggregator is - // shutting down and we want graceful exit - ops.logger.Infof("OperatorPubkeysServiceInMemory: Context cancelled, exiting") - return - case err := <-newPubkeyRegistrationSub.Err(): + newPubkeyRegistrationSub.Unsubscribe() + newPubkeyRegistrationC, newPubkeyRegistrationSub, err = ops.avsRegistrySubscriber.SubscribeToNewPubkeyRegistrations() + if err != nil { ops.logger.Error( - "Error in websocket subscription for new pubkey registration events. Attempting to reconnect...", + "Error opening websocket subscription for new pubkey registrations", "err", err, "service", "OperatorPubkeysServiceInMemory", ) - newPubkeyRegistrationSub.Unsubscribe() - newPubkeyRegistrationC, newPubkeyRegistrationSub, err = ops.avsRegistrySubscriber.SubscribeToNewPubkeyRegistrations() - if err != nil { - ops.logger.Error( - "Error opening websocket subscription for new pubkey registrations", - "err", - err, - "service", - "OperatorPubkeysServiceInMemory", - ) - // see the warning above the struct definition to understand why we panic here - panic(err) - } - case err := <-newSocketRegistrationSub.Err(): + return err + } + case err := <-newSocketRegistrationSub.Err(): + ops.logger.Error( + "Error in websocket subscription for new socket registration events. Attempting to reconnect...", + "err", + err, + "service", + "OperatorPubkeysServiceInMemory", + ) + newSocketRegistrationSub.Unsubscribe() + newSocketRegistrationC, newSocketRegistrationSub, err = ops.avsRegistrySubscriber.SubscribeToOperatorSocketUpdates() + if err != nil { ops.logger.Error( - "Error in websocket subscription for new socket registration events. Attempting to reconnect...", + "Error opening websocket subscription for new socket registrations", "err", err, "service", "OperatorPubkeysServiceInMemory", ) - newSocketRegistrationSub.Unsubscribe() - newSocketRegistrationC, newSocketRegistrationSub, err = ops.avsRegistrySubscriber.SubscribeToOperatorSocketUpdates() - if err != nil { - ops.logger.Error( - "Error opening websocket subscription for new socket registrations", - "err", - err, - "service", - "OperatorPubkeysServiceInMemory", - ) - panic(err) - } - case newPubkeyRegistrationEvent := <-newPubkeyRegistrationC: - operatorAddr := newPubkeyRegistrationEvent.Operator - ops.pubkeyDict[operatorAddr] = types.OperatorPubkeys{ - G1Pubkey: bls.NewG1Point( - newPubkeyRegistrationEvent.PubkeyG1.X, - newPubkeyRegistrationEvent.PubkeyG1.Y, - ), - G2Pubkey: bls.NewG2Point( - newPubkeyRegistrationEvent.PubkeyG2.X, - newPubkeyRegistrationEvent.PubkeyG2.Y, - ), - } + return err + } + case newPubkeyRegistrationEvent := <-newPubkeyRegistrationC: + operatorAddr := newPubkeyRegistrationEvent.Operator + ops.pubkeyDict[operatorAddr] = types.OperatorPubkeys{ + G1Pubkey: bls.NewG1Point( + newPubkeyRegistrationEvent.PubkeyG1.X, + newPubkeyRegistrationEvent.PubkeyG1.Y, + ), + G2Pubkey: bls.NewG2Point( + newPubkeyRegistrationEvent.PubkeyG2.X, + newPubkeyRegistrationEvent.PubkeyG2.Y, + ), + } - operatorId := types.OperatorIdFromContractG1Pubkey(newPubkeyRegistrationEvent.PubkeyG1) - ops.operatorAddrToId[operatorAddr] = operatorId - ops.logger.Debug( - "Added operator pubkeys to pubkey dict from new pubkey registration event", - "service", - "OperatorPubkeysServiceInMemory", - "block", - newPubkeyRegistrationEvent.Raw.BlockNumber, - "operatorAddr", - operatorAddr, - "operatorId", - operatorId, - "G1pubkey", - ops.pubkeyDict[operatorAddr].G1Pubkey, - "G2pubkey", - ops.pubkeyDict[operatorAddr].G2Pubkey, - ) - case newSocketRegistrationEvent := <-newSocketRegistrationC: - ops.logger.Debug( - "Received new socket registration event", - "service", - "OperatorPubkeysServiceInMemory", - "operatorId", - types.OperatorId(newSocketRegistrationEvent.OperatorId), - "socket", - newSocketRegistrationEvent.Socket, - ) - ops.updateSocketMapping( - newSocketRegistrationEvent.OperatorId, - types.Socket(newSocketRegistrationEvent.Socket), - ) - // Receive a query from GetOperatorPubkeys - case query := <-queryC: - pubkeys, ok := ops.pubkeyDict[query.operatorAddr] - operatorId := ops.operatorAddrToId[query.operatorAddr] - socket := ops.socketDict[operatorId] - operatorInfo := types.OperatorInfo{ - Socket: socket, - Pubkeys: pubkeys, - } - query.respC <- resp{operatorInfo, ok} + operatorId := types.OperatorIdFromContractG1Pubkey(newPubkeyRegistrationEvent.PubkeyG1) + ops.operatorAddrToId[operatorAddr] = operatorId + ops.logger.Debug( + "Added operator pubkeys to pubkey dict from new pubkey registration event", + "service", + "OperatorPubkeysServiceInMemory", + "block", + newPubkeyRegistrationEvent.Raw.BlockNumber, + "operatorAddr", + operatorAddr, + "operatorId", + operatorId, + "G1pubkey", + ops.pubkeyDict[operatorAddr].G1Pubkey, + "G2pubkey", + ops.pubkeyDict[operatorAddr].G2Pubkey, + ) + case newSocketRegistrationEvent := <-newSocketRegistrationC: + ops.logger.Debug( + "Received new socket registration event", + "service", + "OperatorPubkeysServiceInMemory", + "operatorId", + types.OperatorId(newSocketRegistrationEvent.OperatorId), + "socket", + newSocketRegistrationEvent.Socket, + ) + ops.updateSocketMapping( + newSocketRegistrationEvent.OperatorId, + types.Socket(newSocketRegistrationEvent.Socket), + ) + // Receive a query from GetOperatorPubkeys + case query := <-queryC: + pubkeys, ok := ops.pubkeyDict[query.operatorAddr] + operatorId := ops.operatorAddrToId[query.operatorAddr] + socket := ops.socketDict[operatorId] + operatorInfo := types.OperatorInfo{ + Socket: socket, + Pubkeys: pubkeys, } + query.respC <- resp{operatorInfo, ok} } - }() + } } func (ops *OperatorsInfoServiceInMemory) queryPastRegisteredOperatorEventsAndFillDb( diff --git a/services/operatorsinfo/operatorsinfo_inmemory_test.go b/services/operatorsinfo/operatorsinfo_inmemory_test.go index 041b5af9..e5ba20c2 100644 --- a/services/operatorsinfo/operatorsinfo_inmemory_test.go +++ b/services/operatorsinfo/operatorsinfo_inmemory_test.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/event" - apkregistrybindings "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" blsapkreg "github.com/Layr-Labs/eigensdk-go/contracts/bindings/BLSApkRegistry" regcoord "github.com/Layr-Labs/eigensdk-go/contracts/bindings/RegistryCoordinator" "github.com/Layr-Labs/eigensdk-go/crypto/bls" @@ -25,14 +24,14 @@ import ( ) type fakeAVSRegistrySubscriber struct { - pubkeyRegistrationEventC chan *apkregistrybindings.ContractBLSApkRegistryNewPubkeyRegistration + pubkeyRegistrationEventC chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration operatorSocketUpdateEventC chan *regcoord.ContractRegistryCoordinatorOperatorSocketUpdate eventSubscription *fakeEventSubscription } func newFakeAVSRegistrySubscriber( eventSubscription *fakeEventSubscription, - pubkeyRegistrationEventC chan *apkregistrybindings.ContractBLSApkRegistryNewPubkeyRegistration, + pubkeyRegistrationEventC chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration, operatorSocketUpdateEventC chan *regcoord.ContractRegistryCoordinatorOperatorSocketUpdate, ) *fakeAVSRegistrySubscriber { return &fakeAVSRegistrySubscriber{ @@ -86,8 +85,8 @@ func TestGetOperatorInfo(t *testing.T) { ContractG2Pubkey: contractG2Pubkey, } - pubkeyRegistrationEventC := make(chan *apkregistrybindings.ContractBLSApkRegistryNewPubkeyRegistration, 1) - pubkeyRegistrationEvent := &apkregistrybindings.ContractBLSApkRegistryNewPubkeyRegistration{ + pubkeyRegistrationEventC := make(chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration, 1) + pubkeyRegistrationEvent := &blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration{ Operator: testOperator1.OperatorAddr, PubkeyG1: testOperator1.ContractG1Pubkey, PubkeyG2: testOperator1.ContractG2Pubkey, @@ -106,7 +105,7 @@ func TestGetOperatorInfo(t *testing.T) { var tests = []struct { name string operator *fakes.TestOperator - pubkeyRegistrationEventC chan *apkregistrybindings.ContractBLSApkRegistryNewPubkeyRegistration + pubkeyRegistrationEventC chan *blsapkreg.ContractBLSApkRegistryNewPubkeyRegistration operatorSocketUpdateEventC chan *regcoord.ContractRegistryCoordinatorOperatorSocketUpdate eventErrC chan error queryOperatorAddr common.Address diff --git a/signer/bls/cerberus/cerberus.go b/signer/bls/cerberus/cerberus.go index c8b87db8..51d6e429 100644 --- a/signer/bls/cerberus/cerberus.go +++ b/signer/bls/cerberus/cerberus.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" sdkBls "github.com/Layr-Labs/eigensdk-go/crypto/bls" "github.com/Layr-Labs/eigensdk-go/signer/bls/types" @@ -21,6 +22,8 @@ type Config struct { URL string PublicKeyHex string + SignerAPIKey string + // Optional: in case if your signer uses local keystore Password string @@ -35,6 +38,7 @@ type Signer struct { kmsClient v1.KeyManagerClient pubKeyHex string password string + signerAPIKey string } func New(cfg Config) (Signer, error) { @@ -61,6 +65,7 @@ func New(cfg Config) (Signer, error) { kmsClient: kmsClient, pubKeyHex: cfg.PublicKeyHex, password: cfg.Password, + signerAPIKey: cfg.SignerAPIKey, }, nil } @@ -69,6 +74,9 @@ func (s Signer) Sign(ctx context.Context, msg []byte) ([]byte, error) { return nil, types.ErrInvalidMessageLength } + // Pass the API key to the signer client + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", s.signerAPIKey) + resp, err := s.signerClient.SignGeneric(ctx, &v1.SignGenericRequest{ Data: msg, PublicKeyG1: s.pubKeyHex, @@ -86,6 +94,9 @@ func (s Signer) SignG1(ctx context.Context, msg []byte) ([]byte, error) { return nil, types.ErrInvalidMessageLength } + // Pass the API key to the signer client + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", s.signerAPIKey) + resp, err := s.signerClient.SignG1(ctx, &v1.SignG1Request{ Data: msg, PublicKeyG1: s.pubKeyHex, diff --git a/signer/bls/signer.go b/signer/bls/signer.go index ccbcb1d2..ed7b8bbc 100644 --- a/signer/bls/signer.go +++ b/signer/bls/signer.go @@ -49,6 +49,7 @@ func NewSigner(cfg types.SignerConfig) (Signer, error) { Password: cfg.CerberusPassword, EnableTLS: cfg.EnableTLS, TLSCertFilePath: cfg.TLSCertFilePath, + SignerAPIKey: cfg.CerberusAPIKey, }) case types.PrivateKey: return privatekey.New(privatekey.Config{ diff --git a/signer/bls/types/types.go b/signer/bls/types/types.go index 8a3e9fa7..2edab232 100644 --- a/signer/bls/types/types.go +++ b/signer/bls/types/types.go @@ -35,4 +35,6 @@ type SignerConfig struct { EnableTLS bool // TLSCertFilePath is the path to the TLS cert file TLSCertFilePath string + // CerberusAPIKey is the API key for the cerberus signer + CerberusAPIKey string } diff --git a/signer/go.mod b/signer/go.mod index 34d47dc2..58ba9627 100644 --- a/signer/go.mod +++ b/signer/go.mod @@ -5,8 +5,9 @@ go 1.21.13 replace github.com/Layr-Labs/eigensdk-go => ../../eigensdk-go require ( - github.com/Layr-Labs/cerberus-api v0.0.2-0.20250108174619-d5e1eb03fbd5 + github.com/Layr-Labs/cerberus-api v0.0.2-0.20250117193600-e69c5e8b08fd github.com/Layr-Labs/eigensdk-go v0.1.13 + github.com/consensys/gnark-crypto v0.12.1 github.com/stretchr/testify v1.9.0 google.golang.org/grpc v1.64.1 ) @@ -15,7 +16,6 @@ require ( github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/consensys/bavard v0.1.13 // indirect - github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect diff --git a/signer/go.sum b/signer/go.sum index 873dadf4..3d523201 100644 --- a/signer/go.sum +++ b/signer/go.sum @@ -1,7 +1,7 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/Layr-Labs/cerberus-api v0.0.2-0.20250108174619-d5e1eb03fbd5 h1:s24M6HYObEuV9OSY36jUM09kp5fOhuz/g1ev2qWDPzU= -github.com/Layr-Labs/cerberus-api v0.0.2-0.20250108174619-d5e1eb03fbd5/go.mod h1:Lm4fhzy0S3P7GjerzuseGaBFVczsIKmEhIjcT52Hluo= +github.com/Layr-Labs/cerberus-api v0.0.2-0.20250117193600-e69c5e8b08fd h1:prMzW4BY6KZtWEanf5EIsyHzIZKCNV2mVIXrE6glRRM= +github.com/Layr-Labs/cerberus-api v0.0.2-0.20250117193600-e69c5e8b08fd/go.mod h1:Lm4fhzy0S3P7GjerzuseGaBFVczsIKmEhIjcT52Hluo= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= diff --git a/signerv2/README.md b/signerv2/README.md index b56ed0be..a43fdc46 100644 --- a/signerv2/README.md +++ b/signerv2/README.md @@ -1,3 +1,59 @@ -# Signerv2 +# Signer v2 -TODO \ No newline at end of file +Signerv2 is a module for signing messages. It provides a simple and unified way to produce cryptographic signatures. +Signers instantiated from this module is required to create some SDK transaction managers (see [`NewPrivateKeyWallet`](../chainio/clients/wallet/privatekey_wallet.go) and [`NewSimpleTxManager`](../chainio/txmgr/simple.go)/[`NewGeometricTxnManager`](../chainio/txmgr/geometric/geometric.go)). + +## Features + +- Sign messages using raw private keys +- Sign messages using encrypted keystores +- Sign messages using a remote signer (web3 or KMS) + +### Comparison to Old Signer + +In comparison to the old signer, Signerv2 offers: + +- New signing mechanisms +- A simplified API for easier extension + +### Using SignerFromConfig + +SignerV2 introduces `SignerFromConfig` + +The `SignerFromConfig` function allows you to create a signer function based on a configuration. +This configuration specifies whether to use a private key signer, a local keystore signer, or a remote web3 signer. + +```go +package main + +import ( + "github.com/Layr-Labs/eigensdk-go/signerv2" +) + +func main() { + config := signerv2.Config{ + // ...initialize your configuration... + } + chainID := // Set your chain ID + signerFn, signerAddr, err := signerv2.SignerFromConfig(config, chainID) + if err != nil { + // Handle error + return + } + // Use signerFn and signerAddr as needed +} +``` + +Internally, `SignerFromConfig` calls different signer functions depending on the config it receives: `PrivateKeySignerFn`, `KeyStoreSignerFn`, or `Web3SignerFn`. +Those functions are also available to users. + +### KMSSignerFn + +This module includes support for signing messages using a Key Management Service (KMS) key. Use `KMSSignerFn` to create a signer for KMS-managed keys. + +## Upgrade from Signer (v1) + +`NewPrivateKeySigner` and `NewPrivateKeyFromKeystoreSigner` functions should be upgraded to use the new `SignerFromConfig` and a `Config` with a `PrivateKey`, or `KeystorePath` and `Password`, respectively. + +The functionality given by the `Signer` interface and `BasicSigner` type was redesigned into the [`wallet`](../chainio/clients/wallet) and [`txmgr`](../chainio/txmgr) modules. +After generating a `SignerFn` as specified in ["UsingSignerFromConfig"](#using-signerfromconfig), you can generate a transaction manager via `NewPrivateKeyWallet` and `NewSimpleTxManager` (or `NewGeometricTxnManager` for geometric gas pricing) diff --git a/testutils/anvil.go b/testutils/anvil.go index 4cda28c7..2de74170 100644 --- a/testutils/anvil.go +++ b/testutils/anvil.go @@ -22,9 +22,10 @@ import ( const ( ANVIL_FIRST_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" ANVIL_FIRST_PRIVATE_KEY = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - ANVIL_SECOND_ADDRESS = "70997970C51812dc3A010C7d01b50e0d17dc79C8" + ANVIL_SECOND_ADDRESS = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" ANVIL_SECOND_PRIVATE_KEY = "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - ANVIL_THIRD_ADDRESS = "3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + ANVIL_THIRD_ADDRESS = "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + ANVIL_THIRD_PRIVATE_KEY = "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" ) // This address is hardcoded because it is required by the elcontracts tests but is not diff --git a/types/test.go b/types/test.go index 01a754b9..298ddaca 100644 --- a/types/test.go +++ b/types/test.go @@ -6,4 +6,5 @@ type TestOperator struct { OperatorId OperatorId StakePerQuorum map[QuorumNum]StakeAmount BlsKeypair *bls.KeyPair + Socket Socket }