diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index fda7c6f..29a5a9f 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -148,8 +148,8 @@ type Subnet struct { // Address of the owner of the Validator Manager Contract ValidatorManagerOwnerAddress *common.Address - // Private key of the owner of the Validator Manager Contract - ValidatorManagerOwnerPrivateKey string + // Signer of the owner of the Validator Manager Contract + ValidatorManagerOwnerSigner *evm.Signer // BootstrapValidators are bootstrap validators that are included in the ConvertSubnetToL1Tx call // that made Subnet a sovereign L1 @@ -364,7 +364,7 @@ func (c *Subnet) Commit(ms multisig.Multisig, wallet wallet.Wallet, waitForTxAcc // to set as the owner of the PoA manager func (c *Subnet) InitializeProofOfAuthority( log logging.Logger, - privateKey string, + signer *evm.Signer, aggregatorLogger logging.Logger, useACP99 bool, signatureAggregatorEndpoint string, @@ -395,7 +395,7 @@ func (c *Subnet) InitializeProofOfAuthority( if client, err := evm.GetClient(c.ValidatorManagerRPC); err != nil { log.Error("failure connecting to Validator Manager RPC to setup proposer VM", zap.Error(err)) } else { - if err := client.SetupProposerVM(privateKey); err != nil { + if err := client.SetupProposerVM(signer); err != nil { log.Error("failure setting proposer VM on Validator Manager's Blockchain", zap.Error(err)) } client.Close() @@ -405,7 +405,7 @@ func (c *Subnet) InitializeProofOfAuthority( log, c.ValidatorManagerRPC, *c.ValidatorManagerAddress, - privateKey, + signer, c.SubnetID, *c.ValidatorManagerOwnerAddress, useACP99, @@ -446,7 +446,7 @@ func (c *Subnet) InitializeProofOfAuthority( log, c.ValidatorManagerRPC, *c.ValidatorManagerAddress, - privateKey, + signer, c.SubnetID, c.ValidatorManagerBlockchainID, c.BootstrapValidators, @@ -461,12 +461,12 @@ func (c *Subnet) InitializeProofOfAuthority( func (c *Subnet) InitializeProofOfStake( log logging.Logger, - privateKey string, + signer *evm.Signer, aggregatorLogger logging.Logger, posParams validatormanager.PoSParams, useACP99 bool, signatureAggregatorEndpoint string, - nativeMinterPrecompileAdminPrivateKey string, + nativeMinterPrecompileAdminSigner *evm.Signer, ) error { if c.Network == network.UndefinedNetwork { return fmt.Errorf("unable to initialize Proof of Stake: %w", errMissingNetwork) @@ -491,13 +491,15 @@ func (c *Subnet) InitializeProofOfStake( if c.ValidatorManagerOwnerAddress == nil { return fmt.Errorf("unable to initialize Proof of Stake: %w", errMissingValidatorManagerOwnerAddress) } - if useACP99 && c.ValidatorManagerOwnerPrivateKey == "" { - return fmt.Errorf("unable to initialize Proof of Stake: %w", errMissingValidatorManagerOwnerPrivateKey) + if useACP99 { + if c.ValidatorManagerOwnerSigner == nil { + return fmt.Errorf("unable to initialize Proof of Stake: %w", errMissingValidatorManagerOwnerPrivateKey) + } } if client, err := evm.GetClient(c.ValidatorManagerRPC); err != nil { log.Error("failure connecting to Validator Manager RPC to setup proposer VM", zap.Error(err)) } else { - if err := client.SetupProposerVM(privateKey); err != nil { + if err := client.SetupProposerVM(signer); err != nil { log.Error("failure setting proposer VM on Validator Manager's Blockchain", zap.Error(err)) } client.Close() @@ -507,7 +509,7 @@ func (c *Subnet) InitializeProofOfStake( log, c.ValidatorManagerRPC, *c.ValidatorManagerAddress, - privateKey, + signer, c.SubnetID, *c.ValidatorManagerOwnerAddress, useACP99, @@ -524,12 +526,12 @@ func (c *Subnet) InitializeProofOfStake( c.ValidatorManagerRPC, *c.ValidatorManagerAddress, *c.SpecializedValidatorManagerAddress, - c.ValidatorManagerOwnerPrivateKey, - privateKey, + c.ValidatorManagerOwnerSigner, + signer, c.SubnetID, posParams, useACP99, - nativeMinterPrecompileAdminPrivateKey, + nativeMinterPrecompileAdminSigner, ) if err != nil { if !errors.Is(err, validatormanager.ErrAlreadyInitialized) { @@ -567,7 +569,7 @@ func (c *Subnet) InitializeProofOfStake( log, c.ValidatorManagerRPC, *c.ValidatorManagerAddress, - privateKey, + signer, c.SubnetID, c.ValidatorManagerBlockchainID, c.BootstrapValidators, diff --git a/evm/contract/contract.go b/evm/contract/contract.go index ed9720f..af708e8 100644 --- a/evm/contract/contract.go +++ b/evm/contract/contract.go @@ -15,7 +15,6 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/hexutil" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/subnet-evm/accounts/abi/bind" "github.com/ava-labs/avalanche-tooling-sdk-go/evm" @@ -291,22 +290,13 @@ func ParseSpec( return name, string(abiBytes), nil } -func idempotentSigner( - _ common.Address, - tx *types.Transaction, -) (*types.Transaction, error) { - return tx, nil -} - // get method name and types from [methodsSpec], then call it // at the smart contract [contractAddress] with the given [params]. // also send [payment] tokens to it func TxToMethod( logger logging.Logger, rpcURL string, - generateRawTxOnly bool, - from common.Address, - privateKey string, + signer *evm.Signer, contractAddress common.Address, payment *big.Int, description string, @@ -314,12 +304,6 @@ func TxToMethod( methodSpec string, params ...interface{}, ) (*types.Transaction, *types.Receipt, error) { - if privateKey == "" && from == (common.Address{}) { - return nil, nil, fmt.Errorf("from address and private key can't be both empty at TxToMethod") - } - if !generateRawTxOnly && privateKey == "" { - return nil, nil, fmt.Errorf("from private key must be defined to be able to sign the tx at TxToMethod") - } methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, payment != nil, false, nil, params...) if err != nil { return nil, nil, err @@ -337,26 +321,16 @@ func TxToMethod( } defer client.Close() contract := bind.NewBoundContract(contractAddress, *abi, client.EthClient, client.EthClient, client.EthClient) - var txOpts *bind.TransactOpts - if generateRawTxOnly { - txOpts = &bind.TransactOpts{ - From: from, - Signer: idempotentSigner, - NoSend: true, - } - } else { - txOpts, err = client.GetTxOptsWithSigner(privateKey) - if err != nil { - return nil, nil, err - } + txOpts, err := client.GetTxOptsWithSigner(signer) + if err != nil { + return nil, nil, err } txOpts.Value = payment tx, err := contract.Transact(txOpts, methodName, params...) if err != nil { trace, traceCallErr := DebugTraceCall( rpcURL, - from, - privateKey, + signer, contractAddress, payment, methodSpec, @@ -376,7 +350,7 @@ func TxToMethod( } return tx, nil, err } - if generateRawTxOnly { + if txOpts.NoSend { return tx, nil, nil } receipt, success, err := client.WaitForTransaction(tx) @@ -403,9 +377,7 @@ func TxToMethod( func TxToMethodWithWarpMessage( logger logging.Logger, rpcURL string, - generateRawTxOnly bool, - from common.Address, - privateKey string, + signer *evm.Signer, contractAddress common.Address, warpMessage *warp.Message, payment *big.Int, @@ -414,12 +386,6 @@ func TxToMethodWithWarpMessage( methodSpec string, params ...interface{}, ) (*types.Transaction, *types.Receipt, error) { - if privateKey == "" && from == (common.Address{}) { - return nil, nil, fmt.Errorf("from address and private key can't be both empty at TxToMethodWithWarpMessage") - } - if !generateRawTxOnly && privateKey == "" { - return nil, nil, fmt.Errorf("from private key must be defined to be able to sign the tx at TxToMethodWithWarpMessage") - } methodName, methodABI, err := ParseSpec(methodSpec, nil, false, false, false, false, nil, params...) if err != nil { return nil, nil, err @@ -441,18 +407,16 @@ func TxToMethodWithWarpMessage( } defer client.Close() tx, err := client.TransactWithWarpMessage( - from, - privateKey, + signer, warpMessage, contractAddress, callData, payment, - generateRawTxOnly, ) if err != nil { return nil, nil, err } - if generateRawTxOnly { + if signer.IsNoOp() { return tx, nil, nil } if err := client.SendTransaction(tx); err != nil { @@ -518,8 +482,7 @@ func handleFailedReceiptStatus( func DebugTraceCall( rpcURL string, - from common.Address, - privateKey string, + signer *evm.Signer, contractAddress common.Address, payment *big.Int, methodSpec string, @@ -545,15 +508,8 @@ func DebugTraceCall( return nil, err } defer client.Close() - if from == (common.Address{}) { - pk, err := crypto.HexToECDSA(privateKey) - if err != nil { - return nil, err - } - from = crypto.PubkeyToAddress(pk.PublicKey) - } data := map[string]string{ - "from": from.Hex(), + "from": signer.Address().Hex(), "to": contractAddress.Hex(), "input": "0x" + hex.EncodeToString(callData), } @@ -613,7 +569,7 @@ func GetSmartContractCallResult[T any](methodName string, out []interface{}) (T, func DeployContract( rpcURL string, - privateKey string, + signer *evm.Signer, binBytes []byte, methodSpec string, params ...interface{}, @@ -639,7 +595,7 @@ func DeployContract( return common.Address{}, nil, nil, err } defer client.Close() - txOpts, err := client.GetTxOptsWithSigner(privateKey) + txOpts, err := client.GetTxOptsWithSigner(signer) if err != nil { return common.Address{}, nil, nil, err } diff --git a/evm/contract/ownable.go b/evm/contract/ownable.go index dd42f0f..7e3fbe3 100644 --- a/evm/contract/ownable.go +++ b/evm/contract/ownable.go @@ -5,6 +5,8 @@ package contract import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/libevm/common" + + "github.com/ava-labs/avalanche-tooling-sdk-go/evm" ) // GetContractOwner gets owner for https://docs.openzeppelin.com/contracts/2.x/api/ownership#Ownable-owner contracts @@ -28,15 +30,13 @@ func TransferOwnership( logger logging.Logger, rpcURL string, contractAddress common.Address, - ownerPrivateKey string, + signer *evm.Signer, newOwner common.Address, ) error { _, _, err := TxToMethod( logger, rpcURL, - false, - common.Address{}, - ownerPrivateKey, + signer, contractAddress, nil, "transfer ownership", diff --git a/evm/evm.go b/evm/evm.go index 1e8f3f7..23331e7 100644 --- a/evm/evm.go +++ b/evm/evm.go @@ -4,7 +4,6 @@ package evm import ( "context" - "crypto/ecdsa" "errors" "fmt" "math/big" @@ -14,7 +13,6 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/subnet-evm/accounts/abi/bind" "github.com/ava-labs/subnet-evm/ethclient" "github.com/ava-labs/subnet-evm/params" @@ -184,16 +182,12 @@ func (client Client) GetContractBytecode( return code, err } -// returns the balance for [privateKey] +// returns the balance for [signer] // supports [repeatsOnFailure] failures -func (client Client) GetPrivateKeyBalance( - privateKey string, +func (client Client) GetSignerBalance( + signer *Signer, ) (*big.Int, error) { - addr, err := PrivateKeyToAddress(privateKey) - if err != nil { - return nil, err - } - return client.GetAddressBalance(addr.Hex()) + return client.GetAddressBalance(signer.Address().Hex()) } // returns the balance for [address] @@ -386,18 +380,14 @@ func (client Client) WaitForTransaction( return nil, false, fmt.Errorf("timeout of %d seconds while waiting for tx %#v on %s: %w", steps, tx, client.URL, cumErr) } -// transfers [amount] to [targetAddressStr] using [sourceAddressPrivateKeyStr] +// transfers [amount] to [targetAddressStr] using [signer] // supports [repeatsOnFailure] failures on each step func (client Client) FundAddress( - sourceAddressPrivateKeyStr string, + signer *Signer, targetAddressStr string, amount *big.Int, ) (*types.Receipt, error) { - sourceAddressPrivateKey, err := crypto.HexToECDSA(sourceAddressPrivateKeyStr) - if err != nil { - return nil, err - } - sourceAddress := crypto.PubkeyToAddress(sourceAddressPrivateKey.PublicKey) + sourceAddress := signer.Address() gasFeeCap, gasTipCap, nonce, err := client.CalculateTxParams(sourceAddress.Hex()) if err != nil { return nil, err @@ -416,8 +406,7 @@ func (client Client) FundAddress( GasTipCap: gasTipCap, Value: amount, }) - txSigner := types.LatestSignerForChainID(chainID) - signedTx, err := types.SignTx(tx, txSigner, sourceAddressPrivateKey) + signedTx, err := signer.SignTx(chainID, tx) if err != nil { return nil, err } @@ -453,20 +442,17 @@ func (client Client) IssueTx( return nil } -// returns tx options that include signer for [prefundedPrivateKeyStr] +// returns tx options that include signer for [signer] // supports [repeatsOnFailure] failures when gathering chain info func (client Client) GetTxOptsWithSigner( - prefundedPrivateKeyStr string, + signer *Signer, ) (*bind.TransactOpts, error) { - prefundedPrivateKey, err := crypto.HexToECDSA(prefundedPrivateKeyStr) - if err != nil { - return nil, err - } chainID, err := client.GetChainID() if err != nil { return nil, fmt.Errorf("failure generating signer: %w", err) } - return bind.NewKeyedTransactorWithChainID(prefundedPrivateKey, chainID) + + return signer.TransactOpts(chainID) } // waits for [timeout] until evm is bootstrapped @@ -488,38 +474,17 @@ func (client Client) WaitForEVMBootstrapped(timeout time.Duration) error { return fmt.Errorf("client at %s not bootstrapped after %.2f seconds: %w", client.URL, timeout.Seconds(), cumErr) } -// generates a transaction signed with [privateKeyStr], calling a [contract] method using [callData] +// generates a transaction signed with [signer], calling a [contract] method using [callData] // including [warpMessage] in the tx accesslist -// if [generateRawTxOnly] is set, it generates a similar, unsigned tx, with given [from] address func (client Client) TransactWithWarpMessage( - from common.Address, - privateKeyStr string, + signer *Signer, warpMessage *avalancheWarp.Message, contract common.Address, callData []byte, value *big.Int, - generateRawTxOnly bool, ) (*types.Transaction, error) { const defaultGasLimit = 2_000_000 - var ( - privateKey *ecdsa.PrivateKey - err error - ) - if privateKeyStr == "" && from == (common.Address{}) { - return nil, fmt.Errorf("from address and private key can't be both empty at GetTxToMethodWithWarpMessage") - } - if !generateRawTxOnly && privateKeyStr == "" { - return nil, fmt.Errorf("from private key must be defined to be able to sign the tx at GetTxToMethodWithWarpMessage") - } - if privateKeyStr != "" { - privateKey, err = crypto.HexToECDSA(privateKeyStr) - if err != nil { - return nil, err - } - if from == (common.Address{}) { - from = crypto.PubkeyToAddress(privateKey.PublicKey) - } - } + from := signer.Address() gasFeeCap, gasTipCap, nonce, err := client.CalculateTxParams(from.Hex()) if err != nil { return nil, err @@ -562,11 +527,7 @@ func (client Client) TransactWithWarpMessage( Data: callData, AccessList: accessList, }) - if generateRawTxOnly { - return tx, nil - } - txSigner := types.LatestSignerForChainID(chainID) - return types.SignTx(tx, txSigner, privateKey) + return signer.SignTx(chainID, tx) } // gets block [n] @@ -663,22 +624,14 @@ func (client Client) WaitForNewBlock( // issue dummy txs to create the given number of blocks func (client Client) CreateDummyBlocks( numBlocks int, - privKeyStr string, + signer *Signer, ) error { - addr, err := PrivateKeyToAddress(privKeyStr) - if err != nil { - return err - } - privKey, err := crypto.HexToECDSA(privKeyStr) - if err != nil { - return err - } + addr := signer.Address() chainID, err := client.GetChainID() if err != nil { return err } gasPrice := big.NewInt(legacy.BaseFee) - txSigner := types.LatestSignerForChainID(chainID) blockNumber, err := client.BlockNumber() if err != nil { return fmt.Errorf("unable to get block number: %w", err) @@ -703,9 +656,9 @@ func (client Client) CreateDummyBlocks( } // send Big1 to himself tx := types.NewTransaction(nonce, addr, common.Big1, ethparams.TxGas, gasPrice, nil) - triggerTx, err := types.SignTx(tx, txSigner, privKey) + triggerTx, err := signer.SignTx(chainID, tx) if err != nil { - return fmt.Errorf("types.SignTx failure at step %d: %w", i, err) + return fmt.Errorf("signer.SignTx failure at step %d: %w", i, err) } if err := client.SendTransaction(triggerTx); err != nil { return fmt.Errorf("client.SendTransaction failure at step %d: %w", i, err) @@ -728,12 +681,12 @@ func (client Client) CreateDummyBlocks( // the current timestamp should be after the ProposerVM activation time (aka ApricotPhase4). // supports [repeatsOnFailure] failures on each step func (client Client) SetupProposerVM( - privKey string, + signer *Signer, ) error { const numBlocks = 2 // Number of blocks needed to activate the proposer VM fork _, err := utils.Retry( func() (any, error) { - return nil, client.CreateDummyBlocks(numBlocks, privKey) + return nil, client.CreateDummyBlocks(numBlocks, signer) }, repeatsOnFailure, sleepBetweenRepeats, diff --git a/evm/evm_test.go b/evm/evm_test.go index 1e2365b..73ab644 100644 --- a/evm/evm_test.go +++ b/evm/evm_test.go @@ -14,7 +14,6 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/warp" "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/subnet-evm/ethclient" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -1236,7 +1235,7 @@ func TestBlockNumber(t *testing.T) { } } -func TestGetPrivateKeyBalance(t *testing.T) { +func TestGetSignerBalance(t *testing.T) { originalSleepBetweenRepeats := sleepBetweenRepeats sleepBetweenRepeats = 1 * time.Millisecond defer func() { @@ -1249,20 +1248,18 @@ func TestGetPrivateKeyBalance(t *testing.T) { EthClient: mockClient, URL: "http://localhost:8545", } - privateKey, err := crypto.GenerateKey() - require.NoError(t, err) - privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey)) - address := crypto.PubkeyToAddress(privateKey.PublicKey) + address := common.HexToAddress("0x1234567890123456789012345678901234567890") + signer := NewNoOpSigner(address) tests := []struct { name string - privateKey string + signer *Signer setupMock func() expected *big.Int expectError bool }{ { - name: "successful balance check", - privateKey: privateKeyHex, + name: "successful balance check", + signer: signer, setupMock: func() { mockClient.EXPECT().BalanceAt(gomock.Any(), address, gomock.Any()). Return(big.NewInt(1000), nil) @@ -1271,8 +1268,8 @@ func TestGetPrivateKeyBalance(t *testing.T) { expectError: false, }, { - name: "error getting balance", - privateKey: privateKeyHex, + name: "error getting balance", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().BalanceAt(gomock.Any(), address, gomock.Any()). @@ -1283,8 +1280,8 @@ func TestGetPrivateKeyBalance(t *testing.T) { expectError: true, }, { - name: "successful after max failures", - privateKey: privateKeyHex, + name: "successful after max failures", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure-1; i++ { mockClient.EXPECT().BalanceAt(gomock.Any(), address, gomock.Any()). @@ -1296,23 +1293,14 @@ func TestGetPrivateKeyBalance(t *testing.T) { expected: big.NewInt(100), expectError: false, }, - { - name: "invalid private key", - privateKey: "invalid", - setupMock: func() {}, - expected: nil, - expectError: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setupMock() - balance, err := client.GetPrivateKeyBalance(tt.privateKey) + balance, err := client.GetSignerBalance(tt.signer) if tt.expectError { require.Error(t, err) - if tt.privateKey != "invalid" { - require.Contains(t, err.Error(), "obtaining balance") - } + require.Contains(t, err.Error(), "obtaining balance") } else { require.NoError(t, err) require.Equal(t, tt.expected, balance) @@ -1438,10 +1426,8 @@ func TestFundAddress(t *testing.T) { EthClient: mockClient, URL: "http://localhost:8545", } - privateKey, err := crypto.GenerateKey() - require.NoError(t, err) - privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey)) - sourceAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + sourceAddress := common.HexToAddress("0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd") + signer := NewNoOpSigner(sourceAddress) targetAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") amount := big.NewInt(1000000000000000000) // 1 ETH tests := []struct { @@ -1546,7 +1532,7 @@ func TestFundAddress(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setupMock() - _, err := client.FundAddress(privateKeyHex, targetAddress.Hex(), amount) + _, err := client.FundAddress(signer, targetAddress.Hex(), amount) if tt.expectError { require.Error(t, err) require.Contains(t, err.Error(), "failure") @@ -1653,44 +1639,48 @@ func TestGetTxOptsWithSigner(t *testing.T) { EthClient: mockClient, URL: "http://localhost:8545", } - privateKey, err := crypto.GenerateKey() - require.NoError(t, err) - privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey)) + signerAddress := common.HexToAddress("0x0102030405060708090a0102030405060708090a") + noOpSigner := NewNoOpSigner(signerAddress) tests := []struct { - name string - privateKey string - setupMock func() - expectError bool + name string + signer *Signer + setupMock func() + expectError bool + errorContains string }{ { - name: "successful signer creation", - privateKey: privateKeyHex, + name: "successful signer creation", + signer: noOpSigner, setupMock: func() { mockClient.EXPECT().ChainID(gomock.Any()). Return(big.NewInt(43114), nil) }, - expectError: false, }, { - name: "invalid private key", - privateKey: "invalid", - setupMock: func() {}, - expectError: true, + name: "invalid signer", + signer: nil, + setupMock: func() { + mockClient.EXPECT().ChainID(gomock.Any()). + Return(big.NewInt(43114), nil) + }, + expectError: true, + errorContains: "signer is nil", }, { - name: "error getting chain ID", - privateKey: privateKeyHex, + name: "error getting chain ID", + signer: noOpSigner, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().ChainID(gomock.Any()). Return(nil, errors.New("failed to get chain ID")) } }, - expectError: true, + expectError: true, + errorContains: "failure generating signer", }, { - name: "successful after max failures", - privateKey: privateKeyHex, + name: "successful after max failures", + signer: noOpSigner, setupMock: func() { for i := 0; i < repeatsOnFailure-1; i++ { mockClient.EXPECT().ChainID(gomock.Any()). @@ -1699,21 +1689,21 @@ func TestGetTxOptsWithSigner(t *testing.T) { mockClient.EXPECT().ChainID(gomock.Any()). Return(big.NewInt(43114), nil) }, - expectError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setupMock() - opts, err := client.GetTxOptsWithSigner(tt.privateKey) + opts, err := client.GetTxOptsWithSigner(tt.signer) if tt.expectError { require.Error(t, err) - if tt.privateKey != "invalid" { - require.Contains(t, err.Error(), "failure generating signer") + if tt.errorContains != "" { + require.Contains(t, err.Error(), tt.errorContains) } } else { require.NoError(t, err) require.NotNil(t, opts) + require.Equal(t, tt.signer.Address(), opts.From) require.NotNil(t, opts.Signer) } }) @@ -1797,10 +1787,6 @@ func TestTransactWithWarpMessage(t *testing.T) { EthClient: mockClient, URL: "http://localhost:8545", } - privateKey, err := crypto.GenerateKey() - require.NoError(t, err) - privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey)) - fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) contractAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") callData := []byte{1, 2, 3, 4, 5} value := big.NewInt(1000000000000000000) // 1 ETH @@ -1811,161 +1797,169 @@ func TestTransactWithWarpMessage(t *testing.T) { warpMessage := &warp.Message{ UnsignedMessage: unsignedMessage, } + primarySignerAddress := common.HexToAddress("0x0102030405060708090a0102030405060708090a") + secondarySignerAddress := common.HexToAddress("0x1112131415161718191a1112131415161718191a") + primarySigner := NewNoOpSigner(primarySignerAddress) + secondarySigner := NewNoOpSigner(secondarySignerAddress) + defaultGasLimit := uint64(2_000_000) tests := []struct { - name string - from common.Address - privateKey string - warpMessage *warp.Message - contract common.Address - callData []byte - value *big.Int - generateRawTxOnly bool - setupMock func() - expectError bool + name string + signer *Signer + warpMessage *warp.Message + contract common.Address + callData []byte + value *big.Int + setupMock func() + expectError bool + errorContains string + expectedGas uint64 }{ { - name: "successful transaction with private key", - from: common.Address{}, - privateKey: privateKeyHex, - warpMessage: warpMessage, - contract: contractAddress, - callData: callData, - value: value, - generateRawTxOnly: false, + name: "successful transaction with signer", + signer: primarySigner, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, setupMock: func() { - // CalculateTxParams mockClient.EXPECT().EstimateBaseFee(gomock.Any()). Return(big.NewInt(10000000000), nil) mockClient.EXPECT().SuggestGasTipCap(gomock.Any()). Return(big.NewInt(1000000000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), fromAddress, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), primarySignerAddress, gomock.Any()). Return(uint64(42), nil) - // GetChainID mockClient.EXPECT().ChainID(gomock.Any()). Return(big.NewInt(43114), nil) - // EstimateGasLimit mockClient.EXPECT().EstimateGas(gomock.Any(), gomock.Any()). Return(uint64(21000), nil) }, - expectError: false, + expectedGas: 21000, }, { - name: "successful raw transaction with from address", - from: fromAddress, - privateKey: "", - warpMessage: warpMessage, - contract: contractAddress, - callData: callData, - value: value, - generateRawTxOnly: true, + name: "successful transaction with alternate signer", + signer: secondarySigner, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, setupMock: func() { - // CalculateTxParams mockClient.EXPECT().EstimateBaseFee(gomock.Any()). Return(big.NewInt(10000000000), nil) mockClient.EXPECT().SuggestGasTipCap(gomock.Any()). Return(big.NewInt(1000000000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), fromAddress, gomock.Any()). - Return(uint64(42), nil) - // GetChainID + mockClient.EXPECT().NonceAt(gomock.Any(), secondarySignerAddress, gomock.Any()). + Return(uint64(7), nil) mockClient.EXPECT().ChainID(gomock.Any()). Return(big.NewInt(43114), nil) - // EstimateGasLimit mockClient.EXPECT().EstimateGas(gomock.Any(), gomock.Any()). - Return(uint64(21000), nil) + Return(uint64(45000), nil) }, - expectError: false, + expectedGas: 45000, }, { - name: "error calculating tx params", - from: common.Address{}, - privateKey: privateKeyHex, - warpMessage: warpMessage, - contract: contractAddress, - callData: callData, - value: value, - generateRawTxOnly: false, + name: "error calculating tx params", + signer: primarySigner, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().EstimateBaseFee(gomock.Any()). Return(nil, errors.New("failed to estimate base fee")) } }, - expectError: true, + expectError: true, + errorContains: "failure", }, { - name: "error getting chain ID", - from: common.Address{}, - privateKey: privateKeyHex, - warpMessage: warpMessage, - contract: contractAddress, - callData: callData, - value: value, - generateRawTxOnly: false, + name: "error getting chain ID", + signer: primarySigner, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, setupMock: func() { - // CalculateTxParams succeeds mockClient.EXPECT().EstimateBaseFee(gomock.Any()). Return(big.NewInt(10000000000), nil) mockClient.EXPECT().SuggestGasTipCap(gomock.Any()). Return(big.NewInt(1000000000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), fromAddress, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), primarySignerAddress, gomock.Any()). Return(uint64(42), nil) - // GetChainID fails for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().ChainID(gomock.Any()). Return(nil, errors.New("failed to get chain ID")) } }, - expectError: true, + expectError: true, + errorContains: "failure", }, { - name: "error estimating gas limit", - from: common.Address{}, - privateKey: privateKeyHex, - warpMessage: warpMessage, - contract: contractAddress, - callData: callData, - value: value, - generateRawTxOnly: false, + name: "error estimating gas limit", + signer: primarySigner, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, setupMock: func() { - // CalculateTxParams succeeds mockClient.EXPECT().EstimateBaseFee(gomock.Any()). Return(big.NewInt(10000000000), nil) mockClient.EXPECT().SuggestGasTipCap(gomock.Any()). Return(big.NewInt(1000000000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), fromAddress, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), primarySignerAddress, gomock.Any()). Return(uint64(42), nil) - // GetChainID succeeds mockClient.EXPECT().ChainID(gomock.Any()). Return(big.NewInt(43114), nil) - // EstimateGasLimit fails for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().EstimateGas(gomock.Any(), gomock.Any()). Return(uint64(0), errors.New("failed to estimate gas")) } }, - expectError: false, // Should use default gas limit + expectedGas: defaultGasLimit, + }, + { + name: "signer failure", + signer: nil, + warpMessage: warpMessage, + contract: contractAddress, + callData: callData, + value: value, + setupMock: func() { + mockClient.EXPECT().EstimateBaseFee(gomock.Any()). + Return(big.NewInt(10000000000), nil) + mockClient.EXPECT().SuggestGasTipCap(gomock.Any()). + Return(big.NewInt(1000000000), nil) + mockClient.EXPECT().NonceAt(gomock.Any(), common.Address{}, gomock.Any()). + Return(uint64(13), nil) + mockClient.EXPECT().ChainID(gomock.Any()). + Return(big.NewInt(43114), nil) + mockClient.EXPECT().EstimateGas(gomock.Any(), gomock.Any()). + Return(uint64(21000), nil) + }, + expectError: true, + errorContains: "signer is nil", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setupMock() tx, err := client.TransactWithWarpMessage( - tt.from, - tt.privateKey, + tt.signer, tt.warpMessage, tt.contract, tt.callData, tt.value, - tt.generateRawTxOnly, ) if tt.expectError { require.Error(t, err) - require.Contains(t, err.Error(), "failure") + if tt.errorContains != "" { + require.Contains(t, err.Error(), tt.errorContains) + } } else { require.NoError(t, err) require.NotNil(t, tx) - if !tt.generateRawTxOnly { - require.NotNil(t, tx.Hash()) + if tt.expectedGas != 0 { + require.Equal(t, tt.expectedGas, tx.Gas()) } } }) @@ -2059,28 +2053,32 @@ func TestSetupProposerVM(t *testing.T) { EthClient: mockClient, URL: "http://localhost:8545", } - privateKey, err := crypto.GenerateKey() - require.NoError(t, err) - privateKeyHex := hex.EncodeToString(crypto.FromECDSA(privateKey)) - address := crypto.PubkeyToAddress(privateKey.PublicKey) chainID := big.NewInt(43114) + signerAddress := common.HexToAddress("0x22232425262728292a2b22232425262728292a2b") + signer := NewNoOpSigner(signerAddress) tests := []struct { - name string - privateKey string - setupMock func() - expectError bool + name string + signer *Signer + setupMock func() + expectError bool + errorContains string }{ { - name: "successful setup", - privateKey: privateKeyHex, + name: "successful setup", + signer: signer, setupMock: func() { // GetChainID mockClient.EXPECT().ChainID(gomock.Any()). Return(chainID, nil) + // Setup + mockClient.EXPECT().BlockNumber(gomock.Any()). + Return(uint64(1000), nil) + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). + Return(uint64(0), nil) // First block mockClient.EXPECT().BlockNumber(gomock.Any()). Return(uint64(1000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), address, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). Return(uint64(0), nil) mockClient.EXPECT().SendTransaction(gomock.Any(), gomock.Any()). Return(nil) @@ -2089,7 +2087,7 @@ func TestSetupProposerVM(t *testing.T) { // Second block mockClient.EXPECT().BlockNumber(gomock.Any()). Return(uint64(1001), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), address, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). Return(uint64(1), nil) mockClient.EXPECT().SendTransaction(gomock.Any(), gomock.Any()). Return(nil) @@ -2099,19 +2097,20 @@ func TestSetupProposerVM(t *testing.T) { expectError: false, }, { - name: "error getting chain ID", - privateKey: privateKeyHex, + name: "error getting chain ID", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure*repeatsOnFailure; i++ { mockClient.EXPECT().ChainID(gomock.Any()). Return(nil, errors.New("failed to get chain ID")) } }, - expectError: true, + expectError: true, + errorContains: "failure", }, { - name: "error getting block number", - privateKey: privateKeyHex, + name: "error getting block number", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { // GetChainID succeeds @@ -2124,11 +2123,12 @@ func TestSetupProposerVM(t *testing.T) { } } }, - expectError: true, + expectError: true, + errorContains: "failure", }, { - name: "error getting nonce", - privateKey: privateKeyHex, + name: "error getting nonce", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { // GetChainID succeeds @@ -2138,25 +2138,31 @@ func TestSetupProposerVM(t *testing.T) { mockClient.EXPECT().BlockNumber(gomock.Any()). Return(uint64(1000), nil) for i := 0; i < repeatsOnFailure; i++ { - mockClient.EXPECT().NonceAt(gomock.Any(), address, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). Return(uint64(0), errors.New("failed to get nonce")) } } }, - expectError: true, + expectError: true, + errorContains: "failure", }, { - name: "error sending transaction", - privateKey: privateKeyHex, + name: "error sending transaction", + signer: signer, setupMock: func() { for i := 0; i < repeatsOnFailure; i++ { // GetChainID succeeds mockClient.EXPECT().ChainID(gomock.Any()). Return(chainID, nil) + // Setup + mockClient.EXPECT().BlockNumber(gomock.Any()). + Return(uint64(1000), nil) + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). + Return(uint64(0), nil) // First block - error sending transaction mockClient.EXPECT().BlockNumber(gomock.Any()). Return(uint64(1000), nil) - mockClient.EXPECT().NonceAt(gomock.Any(), address, gomock.Any()). + mockClient.EXPECT().NonceAt(gomock.Any(), signerAddress, gomock.Any()). Return(uint64(0), nil) for i := 0; i < repeatsOnFailure; i++ { mockClient.EXPECT().SendTransaction(gomock.Any(), gomock.Any()). @@ -2164,23 +2170,18 @@ func TestSetupProposerVM(t *testing.T) { } } }, - expectError: true, - }, - { - name: "invalid private key", - privateKey: "invalid", - setupMock: func() {}, - expectError: true, + expectError: true, + errorContains: "failure", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setupMock() - err := client.SetupProposerVM(tt.privateKey) + err := client.SetupProposerVM(tt.signer) if tt.expectError { require.Error(t, err) - if tt.name != "invalid private key" { - require.Contains(t, err.Error(), "failure") + if tt.errorContains != "" { + require.Contains(t, err.Error(), tt.errorContains) } } else { require.NoError(t, err) diff --git a/evm/precompiles/allowlist.go b/evm/precompiles/allowlist.go index 29275ec..2e94905 100644 --- a/evm/precompiles/allowlist.go +++ b/evm/precompiles/allowlist.go @@ -8,6 +8,7 @@ import ( "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/avalanche-tooling-sdk-go/evm" "github.com/ava-labs/avalanche-tooling-sdk-go/evm/contract" ) @@ -15,15 +16,13 @@ func SetAdmin( logger logging.Logger, rpcURL string, precompile common.Address, - privateKey string, + signer *evm.Signer, toSet common.Address, ) error { _, _, err := contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, precompile, nil, "set precompile admin", @@ -38,15 +37,13 @@ func SetManager( logger logging.Logger, rpcURL string, precompile common.Address, - privateKey string, + signer *evm.Signer, toSet common.Address, ) error { _, _, err := contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, precompile, nil, "set precompile manager", @@ -61,15 +58,13 @@ func SetEnabled( logger logging.Logger, rpcURL string, precompile common.Address, - privateKey string, + signer *evm.Signer, toSet common.Address, ) error { _, _, err := contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, precompile, nil, "set precompile enabled", @@ -84,15 +79,13 @@ func SetNone( logger logging.Logger, rpcURL string, precompile common.Address, - privateKey string, + signer *evm.Signer, toSet common.Address, ) error { _, _, err := contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, precompile, nil, "set precompile none", diff --git a/evm/signer.go b/evm/signer.go new file mode 100644 index 0000000..2baa242 --- /dev/null +++ b/evm/signer.go @@ -0,0 +1,141 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package evm + +import ( + "context" + "fmt" + "math/big" + + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/wallet/chain/c" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/subnet-evm/accounts/abi/bind" + + "github.com/ava-labs/avalanche-tooling-sdk-go/key" +) + +// Signer provides signing capabilities for EVM transactions +// It can be backed by a keychain (for real cryptographic signing) or used in NoOp mode +// (for generating unsigned transactions) +type Signer struct { + signer keychain.Signer // nil when in NoOp mode + addr common.Address +} + +// NewSigner creates a new EVM signer from an avalanchego EthKeychain +// This signer will perform actual cryptographic signing using the keychain +func NewSigner(kc c.EthKeychain) (*Signer, error) { + if kc == nil { + return nil, fmt.Errorf("keychain cannot be nil") + } + + addrs := kc.EthAddresses() + if len(addrs) != 1 { + return nil, fmt.Errorf("expected keychain to have 1 address, found %d", len(addrs)) + } + addr := addrs.List()[0] + + signer, ok := kc.GetEth(addr) + if !ok { + return nil, fmt.Errorf("unexpected failure obtaining unique signer from keychain") + } + + return &Signer{ + signer: signer, + addr: addr, + }, nil +} + +// NewSignerFromPrivateKey creates a new EVM signer from a hex-encoded private key string +// This is a convenience function that handles the key loading and keychain creation +func NewSignerFromPrivateKey(privateKey string) (*Signer, error) { + softKey, err := key.LoadSoftFromBytes([]byte(privateKey)) + if err != nil { + return nil, fmt.Errorf("failed to load private key: %w", err) + } + + return NewSigner(softKey.KeyChain()) +} + +// NewNoOpSigner creates a signer that doesn't actually sign transactions +// Useful for generating raw unsigned transactions where only the "from" address is needed +func NewNoOpSigner(addr common.Address) *Signer { + return &Signer{ + addr: addr, + } +} + +// IsNoOp returns true if this signer is in NoOp mode (doesn't actually sign) +func (s *Signer) IsNoOp() bool { + return s == nil || s.signer == nil +} + +// SignTx signs the provided transaction with the given chainID and returns the signed transaction +// For NoOp signers, returns the transaction unchanged +func (s *Signer) SignTx(chainID *big.Int, tx *types.Transaction) (*types.Transaction, error) { + if s == nil { + return nil, fmt.Errorf("signer is nil") + } + if chainID == nil { + return nil, fmt.Errorf("chainID cannot be nil") + } + if tx == nil { + return nil, fmt.Errorf("transaction cannot be nil") + } + + if s.IsNoOp() { + return tx, nil + } + + txSigner := types.LatestSignerForChainID(chainID) + hash := txSigner.Hash(tx) + + signature, err := s.signer.SignHash(hash.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction hash: %w", err) + } + + return tx.WithSignature(txSigner, signature) +} + +// Address returns the EVM address associated with this signer +func (s *Signer) Address() common.Address { + if s == nil { + return common.Address{} + } + return s.addr +} + +// TransactOpts returns a bind.TransactOpts configured for this signer +func (s *Signer) TransactOpts(chainID *big.Int) (*bind.TransactOpts, error) { + if s == nil { + return nil, fmt.Errorf("signer is nil") + } + if chainID == nil { + return nil, fmt.Errorf("chainID cannot be nil") + } + + opts := &bind.TransactOpts{ + From: s.addr, + Signer: toSignerFn(s, chainID), + Context: context.Background(), + } + + if s.IsNoOp() { + opts.NoSend = true + } + + return opts, nil +} + +// toSignerFn converts an EVM Signer into a bind.SignerFn that can be used with bind.TransactOpts +func toSignerFn(signer *Signer, chainID *big.Int) bind.SignerFn { + return func(address common.Address, tx *types.Transaction) (*types.Transaction, error) { + if address != signer.Address() { + return nil, fmt.Errorf("address mismatch: expected %s, got %s", signer.Address().Hex(), address.Hex()) + } + return signer.SignTx(chainID, tx) + } +} diff --git a/evm/signer_test.go b/evm/signer_test.go new file mode 100644 index 0000000..5996d7d --- /dev/null +++ b/evm/signer_test.go @@ -0,0 +1,375 @@ +// Copyright (C) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. +package evm + +import ( + "errors" + "math/big" + "testing" + + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/types" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + mocks "github.com/ava-labs/avalanche-tooling-sdk-go/mocks/keychain" +) + +var errTest = errors.New("test error") + +func TestNewSigner(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + t.Run("success", func(t *testing.T) { + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + require.NotNil(t, signer) + require.Equal(t, testAddr, signer.Address()) + require.False(t, signer.IsNoOp()) + }) + + t.Run("nil keychain", func(t *testing.T) { + signer, err := NewSigner(nil) + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "keychain cannot be nil") + }) + + t.Run("no addresses in keychain", func(t *testing.T) { + mockKeychain := mocks.NewMockEthKeychain(ctrl) + addrSet := set.Set[common.Address]{} + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + + signer, err := NewSigner(mockKeychain) + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "expected keychain to have 1 address, found 0") + }) + + t.Run("multiple addresses in keychain", func(t *testing.T) { + mockKeychain := mocks.NewMockEthKeychain(ctrl) + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + addrSet.Add(common.HexToAddress("0x9876543210987654321098765432109876543210")) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + + signer, err := NewSigner(mockKeychain) + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "expected keychain to have 1 address, found 2") + }) + + t.Run("GetEth returns false", func(t *testing.T) { + mockKeychain := mocks.NewMockEthKeychain(ctrl) + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(nil, false) + + signer, err := NewSigner(mockKeychain) + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "unexpected failure obtaining unique signer from keychain") + }) +} + +func TestNewNoOpSigner(t *testing.T) { + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + signer := NewNoOpSigner(testAddr) + require.NotNil(t, signer) + require.Equal(t, testAddr, signer.Address()) + require.True(t, signer.IsNoOp()) +} + +func TestSigner_IsNoOp(t *testing.T) { + t.Run("nil signer", func(t *testing.T) { + var signer *Signer + require.True(t, signer.IsNoOp()) + }) + + t.Run("NoOp signer", func(t *testing.T) { + signer := NewNoOpSigner(common.HexToAddress("0x1234567890123456789012345678901234567890")) + require.True(t, signer.IsNoOp()) + }) + + t.Run("crypto signer", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + require.False(t, signer.IsNoOp()) + }) +} + +func TestSigner_Address(t *testing.T) { + t.Run("nil signer", func(t *testing.T) { + var signer *Signer + addr := signer.Address() + require.Equal(t, common.Address{}, addr) + }) + + t.Run("NoOp signer", func(t *testing.T) { + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + signer := NewNoOpSigner(testAddr) + require.Equal(t, testAddr, signer.Address()) + }) + + t.Run("crypto signer", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + require.Equal(t, testAddr, signer.Address()) + }) +} + +func TestSigner_SignTx(t *testing.T) { + chainID := big.NewInt(43114) + testTx := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(100), 21000, big.NewInt(1000000000), nil) + + t.Run("nil signer", func(t *testing.T) { + var signer *Signer + signedTx, err := signer.SignTx(chainID, testTx) + require.Error(t, err) + require.Nil(t, signedTx) + require.Contains(t, err.Error(), "signer is nil") + }) + + t.Run("nil chainID", func(t *testing.T) { + signer := NewNoOpSigner(common.HexToAddress("0x1234567890123456789012345678901234567890")) + signedTx, err := signer.SignTx(nil, testTx) + require.Error(t, err) + require.Nil(t, signedTx) + require.Contains(t, err.Error(), "chainID cannot be nil") + }) + + t.Run("nil transaction", func(t *testing.T) { + signer := NewNoOpSigner(common.HexToAddress("0x1234567890123456789012345678901234567890")) + signedTx, err := signer.SignTx(chainID, nil) + require.Error(t, err) + require.Nil(t, signedTx) + require.Contains(t, err.Error(), "transaction cannot be nil") + }) + + t.Run("NoOp signer returns unsigned tx", func(t *testing.T) { + signer := NewNoOpSigner(common.HexToAddress("0x1234567890123456789012345678901234567890")) + signedTx, err := signer.SignTx(chainID, testTx) + require.NoError(t, err) + require.Equal(t, testTx, signedTx) + }) + + t.Run("crypto signer signs transaction", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + + // Create expected signature (65 bytes with valid v, r, s values) + mockSignature := make([]byte, 65) + mockSignature[64] = 0 // v value + + txSigner := types.LatestSignerForChainID(chainID) + hash := txSigner.Hash(testTx) + + mockSigner.EXPECT().SignHash(hash.Bytes()).Return(mockSignature, nil) + + signedTx, err := signer.SignTx(chainID, testTx) + require.NoError(t, err) + require.NotNil(t, signedTx) + require.NotEqual(t, testTx, signedTx) + }) + + t.Run("crypto signer signing fails", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + + txSigner := types.LatestSignerForChainID(chainID) + hash := txSigner.Hash(testTx) + + mockSigner.EXPECT().SignHash(hash.Bytes()).Return(nil, errTest) + + signedTx, err := signer.SignTx(chainID, testTx) + require.Error(t, err) + require.Nil(t, signedTx) + require.Contains(t, err.Error(), "failed to sign transaction hash") + }) +} + +func TestSigner_TransactOpts(t *testing.T) { + chainID := big.NewInt(43114) + + t.Run("nil signer", func(t *testing.T) { + var signer *Signer + opts, err := signer.TransactOpts(chainID) + require.Error(t, err) + require.Nil(t, opts) + require.Contains(t, err.Error(), "signer is nil") + }) + + t.Run("nil chainID", func(t *testing.T) { + signer := NewNoOpSigner(common.HexToAddress("0x1234567890123456789012345678901234567890")) + opts, err := signer.TransactOpts(nil) + require.Error(t, err) + require.Nil(t, opts) + require.Contains(t, err.Error(), "chainID cannot be nil") + }) + + t.Run("NoOp signer sets NoSend", func(t *testing.T) { + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + signer := NewNoOpSigner(testAddr) + opts, err := signer.TransactOpts(chainID) + require.NoError(t, err) + require.NotNil(t, opts) + require.Equal(t, testAddr, opts.From) + require.True(t, opts.NoSend) + require.NotNil(t, opts.Signer) + require.NotNil(t, opts.Context) + }) + + t.Run("crypto signer doesn't set NoSend", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + + opts, err := signer.TransactOpts(chainID) + require.NoError(t, err) + require.NotNil(t, opts) + require.Equal(t, testAddr, opts.From) + require.False(t, opts.NoSend) + require.NotNil(t, opts.Signer) + require.NotNil(t, opts.Context) + }) + + t.Run("signer function validates address", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockKeychain := mocks.NewMockEthKeychain(ctrl) + mockSigner := mocks.NewMockSigner(ctrl) + testAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + wrongAddr := common.HexToAddress("0x9876543210987654321098765432109876543210") + + addrSet := set.Set[common.Address]{} + addrSet.Add(testAddr) + + mockKeychain.EXPECT().EthAddresses().Return(addrSet) + mockKeychain.EXPECT().GetEth(testAddr).Return(mockSigner, true) + + signer, err := NewSigner(mockKeychain) + require.NoError(t, err) + + opts, err := signer.TransactOpts(chainID) + require.NoError(t, err) + + testTx := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(100), 21000, big.NewInt(1000000000), nil) + + _, err = opts.Signer(wrongAddr, testTx) + require.Error(t, err) + require.Contains(t, err.Error(), "address mismatch") + }) +} + +func TestNewSignerFromPrivateKey(t *testing.T) { + // Using the well-known EWOQ private key for testing + const ewoqPrivateKey = "PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN" + + t.Run("success", func(t *testing.T) { + signer, err := NewSignerFromPrivateKey(ewoqPrivateKey) + require.NoError(t, err) + require.NotNil(t, signer) + require.False(t, signer.IsNoOp()) + + // Verify the address is correct for the EWOQ key + expectedAddr := common.HexToAddress("0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC") + require.Equal(t, expectedAddr, signer.Address()) + }) + + t.Run("invalid private key", func(t *testing.T) { + signer, err := NewSignerFromPrivateKey("invalid-key") + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "failed to load private key") + }) + + t.Run("malformed private key format", func(t *testing.T) { + signer, err := NewSignerFromPrivateKey("PrivateKey-invalid") + require.Error(t, err) + require.Nil(t, signer) + require.Contains(t, err.Error(), "failed to load private key") + }) +} diff --git a/interchain/icm/deployer.go b/interchain/icm/deployer.go index 55f3f8b..caac927 100644 --- a/interchain/icm/deployer.go +++ b/interchain/icm/deployer.go @@ -200,13 +200,13 @@ func (t *Deployer) LoadFromRelease( // If the messenger is already deployed, returns ErrMessengerAlreadyDeployed. func (t *Deployer) Deploy( rpcURL string, - privateKey string, + signer *evm.Signer, ) (string, string, error) { - messengerAddress, err := t.DeployMessenger(rpcURL, privateKey) + messengerAddress, err := t.DeployMessenger(rpcURL, signer) if err != nil { return messengerAddress, "", err } - registryAddress, err := t.DeployRegistry(rpcURL, privateKey) + registryAddress, err := t.DeployRegistry(rpcURL, signer) if err != nil { return messengerAddress, "", err } @@ -218,7 +218,7 @@ func (t *Deployer) Deploy( // Returns the messenger contract address and ErrMessengerAlreadyDeployed if already deployed. func (t *Deployer) DeployMessenger( rpcURL string, - privateKey string, + signer *evm.Signer, ) (string, error) { if err := t.Validate(); err != nil { return "", err @@ -245,7 +245,7 @@ func (t *Deployer) DeployMessenger( toFund := big.NewInt(0). Sub(messengerDeployerRequiredBalance, messengerDeployerBalance) if _, err := client.FundAddress( - privateKey, + signer, t.messengerDeployerAddress, toFund, ); err != nil { @@ -262,7 +262,7 @@ func (t *Deployer) DeployMessenger( // The registry is initialized with the messenger contract address at version 1. func (t *Deployer) DeployRegistry( rpcURL string, - privateKey string, + signer *evm.Signer, ) (string, error) { if err := t.Validate(); err != nil { return "", err @@ -280,7 +280,7 @@ func (t *Deployer) DeployRegistry( } registryAddress, _, _, err := contract.DeployContract( rpcURL, - privateKey, + signer, t.registryBydecode, "([(uint256, address)])", constructorInput, diff --git a/interchain/icm/message.go b/interchain/icm/message.go index 83455d1..5fe661f 100644 --- a/interchain/icm/message.go +++ b/interchain/icm/message.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/avalanche-tooling-sdk-go/evm" "github.com/ava-labs/avalanche-tooling-sdk-go/evm/contract" ) @@ -60,7 +61,7 @@ func SendCrossChainMessage( logger logging.Logger, rpcURL string, messengerAddress common.Address, - privateKey string, + signer *evm.Signer, destinationBlockchainID ids.ID, destinationAddress common.Address, message []byte, @@ -91,9 +92,7 @@ func SendCrossChainMessage( return contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, messengerAddress, nil, "send cross chain message", diff --git a/mocks/keychain/mock_ethkeychain.go b/mocks/keychain/mock_ethkeychain.go new file mode 100644 index 0000000..09d0c4e --- /dev/null +++ b/mocks/keychain/mock_ethkeychain.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/wallet/chain/c (interfaces: EthKeychain) +// +// Generated by this command: +// +// mockgen -destination=mocks/keychain/mock_ethkeychain.go -package=mocks github.com/ava-labs/avalanchego/wallet/chain/c EthKeychain +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + keychain "github.com/ava-labs/avalanchego/utils/crypto/keychain" + set "github.com/ava-labs/avalanchego/utils/set" + common "github.com/ava-labs/libevm/common" + gomock "go.uber.org/mock/gomock" +) + +// MockEthKeychain is a mock of EthKeychain interface. +type MockEthKeychain struct { + ctrl *gomock.Controller + recorder *MockEthKeychainMockRecorder + isgomock struct{} +} + +// MockEthKeychainMockRecorder is the mock recorder for MockEthKeychain. +type MockEthKeychainMockRecorder struct { + mock *MockEthKeychain +} + +// NewMockEthKeychain creates a new mock instance. +func NewMockEthKeychain(ctrl *gomock.Controller) *MockEthKeychain { + mock := &MockEthKeychain{ctrl: ctrl} + mock.recorder = &MockEthKeychainMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEthKeychain) EXPECT() *MockEthKeychainMockRecorder { + return m.recorder +} + +// EthAddresses mocks base method. +func (m *MockEthKeychain) EthAddresses() set.Set[common.Address] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthAddresses") + ret0, _ := ret[0].(set.Set[common.Address]) + return ret0 +} + +// EthAddresses indicates an expected call of EthAddresses. +func (mr *MockEthKeychainMockRecorder) EthAddresses() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthAddresses", reflect.TypeOf((*MockEthKeychain)(nil).EthAddresses)) +} + +// GetEth mocks base method. +func (m *MockEthKeychain) GetEth(addr common.Address) (keychain.Signer, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEth", addr) + ret0, _ := ret[0].(keychain.Signer) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// GetEth indicates an expected call of GetEth. +func (mr *MockEthKeychainMockRecorder) GetEth(addr any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEth", reflect.TypeOf((*MockEthKeychain)(nil).GetEth), addr) +} diff --git a/mocks/keychain/mock_signer.go b/mocks/keychain/mock_signer.go new file mode 100644 index 0000000..d014513 --- /dev/null +++ b/mocks/keychain/mock_signer.go @@ -0,0 +1,85 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/utils/crypto/keychain (interfaces: Signer) +// +// Generated by this command: +// +// mockgen -destination=mocks/keychain/mock_signer.go -package=mocks github.com/ava-labs/avalanchego/utils/crypto/keychain Signer +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + ids "github.com/ava-labs/avalanchego/ids" + gomock "go.uber.org/mock/gomock" +) + +// MockSigner is a mock of Signer interface. +type MockSigner struct { + ctrl *gomock.Controller + recorder *MockSignerMockRecorder + isgomock struct{} +} + +// MockSignerMockRecorder is the mock recorder for MockSigner. +type MockSignerMockRecorder struct { + mock *MockSigner +} + +// NewMockSigner creates a new mock instance. +func NewMockSigner(ctrl *gomock.Controller) *MockSigner { + mock := &MockSigner{ctrl: ctrl} + mock.recorder = &MockSignerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSigner) EXPECT() *MockSignerMockRecorder { + return m.recorder +} + +// Address mocks base method. +func (m *MockSigner) Address() ids.ShortID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Address") + ret0, _ := ret[0].(ids.ShortID) + return ret0 +} + +// Address indicates an expected call of Address. +func (mr *MockSignerMockRecorder) Address() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockSigner)(nil).Address)) +} + +// Sign mocks base method. +func (m *MockSigner) Sign(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sign", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Sign indicates an expected call of Sign. +func (mr *MockSignerMockRecorder) Sign(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*MockSigner)(nil).Sign), arg0) +} + +// SignHash mocks base method. +func (m *MockSigner) SignHash(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignHash", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SignHash indicates an expected call of SignHash. +func (mr *MockSignerMockRecorder) SignHash(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignHash", reflect.TypeOf((*MockSigner)(nil).SignHash), arg0) +} diff --git a/validatormanager/root.go b/validatormanager/root.go index 128de12..2eccc46 100644 --- a/validatormanager/root.go +++ b/validatormanager/root.go @@ -14,6 +14,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/avalanche-tooling-sdk-go/evm" "github.com/ava-labs/avalanche-tooling-sdk-go/evm/contract" "github.com/ava-labs/avalanche-tooling-sdk-go/network" "github.com/ava-labs/avalanche-tooling-sdk-go/validatormanager/validatormanagertypes" @@ -253,7 +254,7 @@ func InitializeValidatorsSet( logger logging.Logger, rpcURL string, managerAddress common.Address, - privateKey string, + signer *evm.Signer, subnetID ids.ID, managerBlockchainID ids.ID, convertSubnetValidators []*txs.ConvertSubnetToL1Validator, @@ -287,9 +288,7 @@ func InitializeValidatorsSet( return contract.TxToMethodWithWarpMessage( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, managerAddress, subnetConversionSignedMessage, big.NewInt(0), diff --git a/validatormanager/validator_manager_poa.go b/validatormanager/validator_manager_poa.go index 441c960..7bc5a01 100644 --- a/validatormanager/validator_manager_poa.go +++ b/validatormanager/validator_manager_poa.go @@ -9,6 +9,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/avalanche-tooling-sdk-go/evm" "github.com/ava-labs/avalanche-tooling-sdk-go/evm/contract" ) @@ -19,7 +20,7 @@ func PoAValidatorManagerInitialize( logger logging.Logger, rpcURL string, managerAddress common.Address, - privateKey string, + signer *evm.Signer, subnetID ids.ID, ownerAddress common.Address, useACP99 bool, @@ -28,9 +29,7 @@ func PoAValidatorManagerInitialize( return contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, managerAddress, nil, "initialize PoA manager", @@ -47,9 +46,7 @@ func PoAValidatorManagerInitialize( return contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, managerAddress, nil, "initialize PoA manager", diff --git a/validatormanager/validator_manager_pos.go b/validatormanager/validator_manager_pos.go index 1a99530..b724b15 100644 --- a/validatormanager/validator_manager_pos.go +++ b/validatormanager/validator_manager_pos.go @@ -24,12 +24,12 @@ func PoSValidatorManagerInitialize( rpcURL string, managerAddress common.Address, specializedManagerAddress common.Address, - managerOwnerPrivateKey string, - privateKey string, + managerOwnerSigner *evm.Signer, + signer *evm.Signer, subnetID [32]byte, posParams PoSParams, useACP99 bool, - nativeMinterPrecompileAdminPrivateKey string, + nativeMinterPrecompileAdminSigner *evm.Signer, ) (*types.Transaction, *types.Receipt, error) { if err := posParams.Verify(); err != nil { return nil, nil, err @@ -55,14 +55,14 @@ func PoSValidatorManagerInitialize( return nil, nil, err } if allowedStatus.Cmp(big.NewInt(0)) == 0 { - if nativeMinterPrecompileAdminPrivateKey == "" { + if nativeMinterPrecompileAdminSigner == nil { return nil, nil, fmt.Errorf("no managed native minter precompile admin was found, and need to be used to enable Native PoS") } if err := precompiles.SetEnabled( logger, rpcURL, precompiles.NativeMinterPrecompile, - nativeMinterPrecompileAdminPrivateKey, + nativeMinterPrecompileAdminSigner, specializedManagerAddress, ); err != nil { return nil, nil, err @@ -77,9 +77,7 @@ func PoSValidatorManagerInitialize( if tx, receipt, err := contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, specializedManagerAddress, nil, "initialize Native Token PoS manager", @@ -99,12 +97,9 @@ func PoSValidatorManagerInitialize( ); err != nil { return tx, receipt, err } - managerOwnerAddress, err := evm.PrivateKeyToAddress(managerOwnerPrivateKey) - if err != nil { - return nil, nil, err - } + managerOwnerAddress := managerOwnerSigner.Address() _, err = client.FundAddress( - privateKey, + signer, managerOwnerAddress.Hex(), big.NewInt(100_000_000_000_000_000), // 0.1 TOKEN ) @@ -115,7 +110,7 @@ func PoSValidatorManagerInitialize( logger, rpcURL, managerAddress, - managerOwnerPrivateKey, + managerOwnerSigner, specializedManagerAddress, ) return nil, nil, err @@ -123,9 +118,7 @@ func PoSValidatorManagerInitialize( return contract.TxToMethod( logger, rpcURL, - false, - common.Address{}, - privateKey, + signer, managerAddress, nil, "initialize Native Token PoS manager",