Skip to content

Commit

Permalink
feat: redesign Signer and replace BaseSigner with ECDSASigner
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The `Signer` interface has been redesigned to be minimal and more
extensible. The `BaseSigner` has been replaced by `ECDSASigner`. Additionally,
`Wallet`, `WalletL1`, and `WalletL2` now use `ECDSASigner` in their constructors.
  • Loading branch information
danijelTxFusion committed Sep 29, 2024
1 parent c2aa297 commit 00901f5
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 384 deletions.
97 changes: 50 additions & 47 deletions accounts/signer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package accounts

import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/ethereum/go-ethereum/accounts"
Expand All @@ -9,41 +10,35 @@ import (
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/pkg/errors"
"github.com/stephenlacy/go-ethereum-hdwallet"
"github.com/zksync-sdk/zksync2-go/eip712"
"github.com/zksync-sdk/zksync2-go/types"
"math/big"
)

// Signer provides support for signing EIP-712 transactions as well as other types of transactions supported by
// types.Signer.
// Signer provides support for signing various types of payloads using some kind of secret.
type Signer interface {
// Address returns the address associated with the signer.
Address() common.Address
// Domain returns the EIP-712 domain used for signing.
Domain() *eip712.Domain
// PrivateKey returns the private key associated with the signer.
PrivateKey() *ecdsa.PrivateKey
// SignHash signs the given hash using the signer's private key and returns the signature.
// The hash should be the 32-byte hash of the data to be signed.
SignHash(msg []byte) ([]byte, error)
// SignTypedData signs the given EIP-712 typed data using the signer's private key and returns the signature.
// The domain parameter is the EIP-712 domain separator, and the data parameter is the EIP-712 typed data.
SignTypedData(typedData apitypes.TypedData) ([]byte, error)
// SignMessage sings an arbitrary message.
SignMessage(ctx context.Context, message []byte) ([]byte, error)
// SignTransaction signs the given transaction.
SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, error)
// SignTypedData signs the given EIP712 typed data.
SignTypedData(ctx context.Context, typedData *apitypes.TypedData) ([]byte, error)
}

// BaseSigner represents basis implementation of Signer interface.
type BaseSigner struct {
// ECDSASigner represents basis implementation of Signer interface.
type ECDSASigner struct {
pk *ecdsa.PrivateKey
address common.Address
domain *eip712.Domain
chainId *big.Int
}

// NewBaseSignerFromMnemonic creates a new instance of BaseSigner based on the provided mnemonic phrase.
func NewBaseSignerFromMnemonic(mnemonic string, chainId int64) (*BaseSigner, error) {
return NewBaseSignerFromMnemonicAndAccountId(mnemonic, 0, chainId)
// NewECDSASignerFromMnemonic creates a new instance of ECDSASigner based on the provided mnemonic phrase.
func NewECDSASignerFromMnemonic(mnemonic string, chainId *big.Int) (*ECDSASigner, error) {
return NewECDSASignerFromMnemonicAndAccountId(mnemonic, 0, chainId)
}

// NewBaseSignerFromMnemonicAndAccountId creates a new instance of BaseSigner based on the provided mnemonic phrase and
// NewECDSASignerFromMnemonicAndAccountId creates a new instance of ECDSASigner based on the provided mnemonic phrase and
// account ID.
func NewBaseSignerFromMnemonicAndAccountId(mnemonic string, accountId uint32, chainId int64) (*BaseSigner, error) {
func NewECDSASignerFromMnemonicAndAccountId(mnemonic string, accountId uint32, chainId *big.Int) (*ECDSASigner, error) {
wallet, err := hdwallet.NewFromMnemonic(mnemonic)
if err != nil {
return nil, errors.Wrap(err, "failed to create HD wallet from mnemonic")
Expand All @@ -61,29 +56,29 @@ func NewBaseSignerFromMnemonicAndAccountId(mnemonic string, accountId uint32, ch
return nil, errors.Wrap(err, "failed to get account's private key from HD wallet")
}
pub := pk.Public().(*ecdsa.PublicKey)
return &BaseSigner{
return &ECDSASigner{
pk: pk,
address: crypto.PubkeyToAddress(*pub),
domain: eip712.ZkSyncEraEIP712Domain(chainId),
chainId: chainId,
}, nil
}

// NewBaseSignerFromRawPrivateKey creates a new instance of BaseSigner based on the provided raw private key.
func NewBaseSignerFromRawPrivateKey(rawPk []byte, chainId int64) (*BaseSigner, error) {
// NewECDSASignerFromRawPrivateKey creates a new instance of ECDSASigner based on the provided raw private key.
func NewECDSASignerFromRawPrivateKey(rawPk []byte, chainId *big.Int) (*ECDSASigner, error) {
pk, err := crypto.ToECDSA(rawPk)
if err != nil {
return nil, errors.Wrap(err, "invalid raw private key")
}
pub := pk.Public().(*ecdsa.PublicKey)
return &BaseSigner{
return &ECDSASigner{
pk: pk,
address: crypto.PubkeyToAddress(*pub),
domain: eip712.ZkSyncEraEIP712Domain(chainId),
chainId: chainId,
}, nil
}

// NewRandomBaseSigner creates an instance of Signer with a randomly generated private key.
func NewRandomBaseSigner(chainId int64) (*BaseSigner, error) {
func NewRandomBaseSigner(chainId *big.Int) (*ECDSASigner, error) {
privateKey, err := crypto.GenerateKey()
if err != nil {
return nil, fmt.Errorf("failed to generate radnom private key: %w", err)
Expand All @@ -92,27 +87,43 @@ func NewRandomBaseSigner(chainId int64) (*BaseSigner, error) {
if !ok {
return nil, fmt.Errorf("failed to convert public key to ECDSA")
}
return &BaseSigner{
return &ECDSASigner{
pk: privateKey,
address: crypto.PubkeyToAddress(*publicKey),
domain: eip712.ZkSyncEraEIP712Domain(chainId),
chainId: chainId,
}, nil
}

func (s *BaseSigner) Address() common.Address {
func (s *ECDSASigner) Address() common.Address {
return s.address
}

func (s *BaseSigner) Domain() *eip712.Domain {
return s.domain
func (s *ECDSASigner) ChainID() *big.Int {
return s.chainId
}

func (s *BaseSigner) PrivateKey() *ecdsa.PrivateKey {
func (s *ECDSASigner) PrivateKey() *ecdsa.PrivateKey {
return s.pk
}

func (s *BaseSigner) SignTypedData(typedData apitypes.TypedData) ([]byte, error) {
hash, err := s.HashTypedData(typedData)
func (s *ECDSASigner) SignMessage(_ context.Context, msg []byte) ([]byte, error) {
sig, err := crypto.Sign(msg, s.pk)
if err != nil {
return nil, errors.Wrap(err, "failed to sign hash")
}
return sig, nil
}

func (s *ECDSASigner) SignTransaction(ctx context.Context, tx *types.Transaction) ([]byte, error) {
typedData, err := tx.TypedData()
if err != nil {
return nil, err
}
return s.SignTypedData(ctx, typedData)
}

func (s *ECDSASigner) SignTypedData(_ context.Context, typedData *apitypes.TypedData) ([]byte, error) {
hash, err := s.hashTypedData(typedData)
if err != nil {
return nil, fmt.Errorf("failed to get hash of typed data: %w", err)
}
Expand All @@ -128,7 +139,7 @@ func (s *BaseSigner) SignTypedData(typedData apitypes.TypedData) ([]byte, error)
return sig, nil
}

func (s *BaseSigner) HashTypedData(data apitypes.TypedData) ([]byte, error) {
func (s *ECDSASigner) hashTypedData(data *apitypes.TypedData) ([]byte, error) {
domain, err := data.HashStruct("EIP712Domain", data.Domain.Map())
if err != nil {
return nil, fmt.Errorf("failed to get hash of typed data domain: %w", err)
Expand All @@ -141,11 +152,3 @@ func (s *BaseSigner) HashTypedData(data apitypes.TypedData) ([]byte, error) {
prefixedDataHash := crypto.Keccak256(prefixedData)
return prefixedDataHash, nil
}

func (s *BaseSigner) SignHash(msg []byte) ([]byte, error) {
sig, err := crypto.Sign(msg, s.pk)
if err != nil {
return nil, errors.Wrap(err, "failed to sign hash")
}
return sig, nil
}
6 changes: 3 additions & 3 deletions accounts/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,19 @@ func ensureTransactOpts(auth *TransactOpts) *TransactOpts {
return auth
}

func newTransactorWithSigner(signer *Signer, chainID *big.Int) (*bind.TransactOpts, error) {
func newTransactorWithSigner(signer *ECDSASigner, chainID *big.Int) (*bind.TransactOpts, error) {
if chainID == nil {
return nil, bind.ErrNoChainID
}
keyAddr := (*signer).Address()
keyAddr := signer.Address()
latestSigner := types.LatestSignerForChainID(chainID)
return &bind.TransactOpts{
From: keyAddr,
Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {
if address != keyAddr {
return nil, bind.ErrNotAuthorized
}
signature, err := (*signer).SignHash(latestSigner.Hash(tx).Bytes())
signature, err := signer.SignMessage(context.Background(), latestSigner.Hash(tx).Bytes())
if err != nil {
return nil, err
}
Expand Down
32 changes: 13 additions & 19 deletions accounts/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ func NewWallet(rawPrivateKey []byte, clientL2 *clients.Client, clientL1 *ethclie
if err != nil {
return nil, err
}
signer, err := NewBaseSignerFromRawPrivateKey(rawPrivateKey, chainID.Int64())
signer, err := NewECDSASignerFromRawPrivateKey(rawPrivateKey, chainID)
if err != nil {
return nil, err
}
s := Signer(signer)
return NewWalletFromSigner(&s, clientL2, clientL1)
return NewWalletFromSigner(signer, clientL2, clientL1)
}

// NewWalletFromSigner creates an instance of Wallet associated with the account provided by the signer.
Expand All @@ -42,7 +41,7 @@ func NewWallet(rawPrivateKey []byte, clientL2 *clients.Client, clientL1 *ethclie
// require communication with the network.
// A runner that contains only a signer can be configured to communicate with L2 and L1 networks by
// using Wallet.Connect and Wallet.ConnectL1, respectively.
func NewWalletFromSigner(signer *Signer, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
func NewWalletFromSigner(signer *ECDSASigner, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
if signer == nil {
return nil, errors.New("signer must be provided")
}
Expand Down Expand Up @@ -73,38 +72,35 @@ func NewWalletFromSigner(signer *Signer, clientL2 *clients.Client, clientL1 *eth
// NewWalletFromMnemonic creates a new instance of Wallet based on the provided mnemonic phrase.
// The clientL2 and clientL1 parameters are optional, and can be configured with Wallet.Connect
// and Wallet.ConnectL1, respectively.
func NewWalletFromMnemonic(mnemonic string, chainId int64, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
signer, err := NewBaseSignerFromMnemonic(mnemonic, chainId)
func NewWalletFromMnemonic(mnemonic string, chainId *big.Int, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
signer, err := NewECDSASignerFromMnemonic(mnemonic, chainId)
if err != nil {
return nil, err
}
s := Signer(signer)
return NewWalletFromSigner(&s, clientL2, clientL1)
return NewWalletFromSigner(signer, clientL2, clientL1)
}

// NewWalletFromRawPrivateKey creates a new instance of Wallet based on the provided private key
// of the account and chain ID.
// The clientL2 and clientL1 parameters are optional, and can be configured with Wallet.Connect
// and Wallet.ConnectL1, respectively.
func NewWalletFromRawPrivateKey(rawPk []byte, chainId int64, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
signer, err := NewBaseSignerFromRawPrivateKey(rawPk, chainId)
func NewWalletFromRawPrivateKey(rawPk []byte, chainId *big.Int, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
signer, err := NewECDSASignerFromRawPrivateKey(rawPk, chainId)
if err != nil {
return nil, err
}
s := Signer(signer)
return NewWalletFromSigner(&s, clientL2, clientL1)
return NewWalletFromSigner(signer, clientL2, clientL1)
}

// NewRandomWallet creates an instance of Wallet with a randomly generated account.
// The clientL2 and clientL1 parameters are optional, and can be configured with Wallet.Connect
// and Wallet.ConnectL1, respectively.
func NewRandomWallet(chainId int64, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
func NewRandomWallet(chainId *big.Int, clientL2 *clients.Client, clientL1 *ethclient.Client) (*Wallet, error) {
signer, err := NewRandomBaseSigner(chainId)
if err != nil {
return nil, err
}
s := Signer(signer)
return NewWalletFromSigner(&s, clientL2, clientL1)
return NewWalletFromSigner(signer, clientL2, clientL1)
}

// Nonce returns the account nonce of the associated account.
Expand All @@ -121,12 +117,10 @@ func (w *Wallet) PendingNonce(ctx context.Context) (uint64, error) {

// Connect returns a new instance of Wallet with the provided client for the L2 network.
func (w *Wallet) Connect(client *clients.Client) (*Wallet, error) {
s := w.Signer()
return NewWalletFromSigner(&s, client, w.clientL1)
return NewWalletFromSigner(w.Signer(), client, w.clientL1)
}

// ConnectL1 returns a new instance of Wallet with the provided client for the L1 network.
func (w *Wallet) ConnectL1(client *ethclient.Client) (*Wallet, error) {
s := w.Signer()
return NewWalletFromSigner(&s, w.clientL2, client)
return NewWalletFromSigner(w.Signer(), w.clientL2, client)
}
Loading

0 comments on commit 00901f5

Please sign in to comment.