75b02dff3d
* op-signer: add to this repo * circleci: add op-signer jobs/workflows * ops: update tag service to include op-signer * readme: add op-signer one sentence description * ci: add op-signer option to github action * ops: add op-signer min version
183 lines
5.9 KiB
Go
183 lines
5.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"math/big"
|
|
"testing"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/holiman/uint256"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
|
|
"github.com/ethereum-optimism/infra/op-signer/service/provider"
|
|
clientSigner "github.com/ethereum-optimism/optimism/op-service/signer"
|
|
)
|
|
|
|
func createEIP1559Tx() *types.Transaction {
|
|
aa := common.HexToAddress("0x000000000000000000000000000000000000aaaa")
|
|
accesses := types.AccessList{types.AccessTuple{
|
|
Address: aa,
|
|
StorageKeys: []common.Hash{{0}},
|
|
}}
|
|
txdata := &types.DynamicFeeTx{
|
|
ChainID: params.AllEthashProtocolChanges.ChainID,
|
|
Nonce: 0,
|
|
To: &aa,
|
|
Gas: 30000,
|
|
GasFeeCap: big.NewInt(1),
|
|
GasTipCap: big.NewInt(1),
|
|
AccessList: accesses,
|
|
Data: []byte{},
|
|
Value: big.NewInt(1),
|
|
}
|
|
tx := types.NewTx(txdata)
|
|
return tx
|
|
}
|
|
|
|
func createBlobTx() *types.Transaction {
|
|
aa := common.HexToAddress("0x000000000000000000000000000000000000aaaa")
|
|
accesses := types.AccessList{types.AccessTuple{
|
|
Address: aa,
|
|
StorageKeys: []common.Hash{{0}},
|
|
}}
|
|
|
|
txdata := &types.BlobTx{
|
|
ChainID: uint256.MustFromBig(params.AllEthashProtocolChanges.ChainID),
|
|
Nonce: 0,
|
|
To: aa,
|
|
Gas: 30000,
|
|
GasFeeCap: uint256.NewInt(1),
|
|
GasTipCap: uint256.NewInt(1),
|
|
AccessList: accesses,
|
|
Data: []byte{},
|
|
Value: uint256.NewInt(1),
|
|
BlobFeeCap: uint256.NewInt(1),
|
|
BlobHashes: []common.Hash{common.HexToHash("c0ffee")},
|
|
}
|
|
tx := types.NewTx(txdata)
|
|
return tx
|
|
}
|
|
|
|
var config = SignerServiceConfig{
|
|
Auth: []AuthConfig{
|
|
{ClientName: "client.oplabs.co", KeyName: "keyName"},
|
|
{ClientName: "alt-client.oplabs.co", KeyName: "altKeyName"},
|
|
{ClientName: "authorized-to.oplabs.co", KeyName: "keyName", ToAddresses: []string{"0x000000000000000000000000000000000000Aaaa"}},
|
|
{ClientName: "unauthorized-to.oplabs.co", KeyName: "keyName", ToAddresses: []string{"0x000000000000000000000000000000000000bbbb"}},
|
|
{ClientName: "within-max-value.oplabs.co", KeyName: "keyName", MaxValue: hexutil.EncodeBig(big.NewInt(2))},
|
|
{ClientName: "exceeds-max-value.oplabs.co", KeyName: "keyName", MaxValue: hexutil.EncodeBig(big.NewInt(0))},
|
|
},
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
template func() *types.Transaction
|
|
}
|
|
|
|
var testTxs = []testCase{
|
|
{"regular", createEIP1559Tx},
|
|
{"blob-tx", createBlobTx},
|
|
}
|
|
|
|
func TestSignTransaction(t *testing.T) {
|
|
for _, tc := range testTxs {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testSignTransaction(t, tc.template())
|
|
})
|
|
}
|
|
}
|
|
|
|
func testSignTransaction(t *testing.T, tx *types.Transaction) {
|
|
ctrl := gomock.NewController(t)
|
|
defer ctrl.Finish()
|
|
|
|
signer := types.LatestSignerForChainID(tx.ChainId())
|
|
digest := signer.Hash(tx).Bytes()
|
|
|
|
priv, err := crypto.GenerateKey()
|
|
require.NoError(t, err)
|
|
|
|
sender := crypto.PubkeyToAddress(priv.PublicKey)
|
|
|
|
signature, err := crypto.Sign(digest, priv)
|
|
require.NoError(t, err)
|
|
|
|
args := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
|
|
missingNonce := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
|
|
missingNonce.Nonce = nil
|
|
|
|
validFrom := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
|
|
validFrom.From = &sender
|
|
|
|
invalidFrom := clientSigner.NewTransactionArgsFromTransaction(tx.ChainId(), nil, tx)
|
|
random := common.HexToAddress("1234")
|
|
invalidFrom.From = &random
|
|
|
|
// signature, _ := hexutil.Decode("0x5392c93b50eb9e3412ab43d378048d4f7d644f3cea02acb529f07e2babba1d3a332377f4abe24a40030b3ff6bff3413a44364aad4665f4e24117466328ce8d3600")
|
|
|
|
tests := []struct {
|
|
testName string
|
|
args clientSigner.TransactionArgs
|
|
digest []byte
|
|
clientName string
|
|
wantKeyName string
|
|
wantErrCode int
|
|
}{
|
|
{"happy path", *args, digest, "client.oplabs.co", "keyName", 0},
|
|
{"nonce not specified", *missingNonce, digest, "client.oplabs.co", "keyName", -32010},
|
|
{"happy path - different client and key", *args, digest, "alt-client.oplabs.co", "altKeyName", 0},
|
|
{"client not authorized", *args, digest, "forbidden-client.oplabs.co", "keyName", 403},
|
|
{"client empty", *args, digest, "", "", 403},
|
|
{"authorized to address", *args, digest, "authorized-to.oplabs.co", "keyName", 0},
|
|
{"unauthorized to address", *args, digest, "unauthorized-to.oplabs.co", "keyName", -32011},
|
|
{"within max value", *args, digest, "within-max-value.oplabs.co", "keyName", 0},
|
|
{"exceeds max value", *args, digest, "exceeds-max-value.oplabs.co", "keyName", -32011},
|
|
{"valid from", *validFrom, digest, "client.oplabs.co", "keyName", 0},
|
|
{"invalid from", *invalidFrom, digest, "client.oplabs.co", "keyName", -32010},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.testName, func(t *testing.T) {
|
|
mockSignatureProvider := provider.NewMockSignatureProvider(ctrl)
|
|
service := NewSignerServiceWithProvider(log.Root(), config, mockSignatureProvider)
|
|
|
|
ctx := context.WithValue(context.TODO(), clientInfoContextKey{}, ClientInfo{ClientName: tt.clientName})
|
|
if tt.wantErrCode == 0 || tt.testName == "invalid from" {
|
|
mockSignatureProvider.EXPECT().
|
|
SignDigest(ctx, tt.wantKeyName, tt.digest).
|
|
Return(signature, nil)
|
|
}
|
|
resp, err := service.SignTransaction(ctx, tt.args)
|
|
if tt.wantErrCode == 0 {
|
|
assert.Nil(t, err)
|
|
if assert.NotNil(t, resp) {
|
|
assert.NotEmpty(t, resp)
|
|
}
|
|
} else {
|
|
assert.NotNil(t, err)
|
|
assert.Nil(t, resp)
|
|
var rpcErr rpc.Error
|
|
var httpErr rpc.HTTPError
|
|
if errors.As(err, &rpcErr) {
|
|
assert.Equal(t, tt.wantErrCode, rpcErr.ErrorCode())
|
|
} else if errors.As(err, &httpErr) {
|
|
assert.Equal(t, tt.wantErrCode, httpErr.StatusCode)
|
|
} else {
|
|
assert.Fail(t, "returned error is not an rpc.Error or rpc.HTTPError")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|