From 652572ecf7627229da6ce3c76f0603cb8205149f Mon Sep 17 00:00:00 2001 From: aaronbuchwald Date: Tue, 29 Nov 2022 13:39:16 -0500 Subject: [PATCH] Migrate replay protection bypass for Nick's Method Signature txs (#377) --- core/types/transaction_signing_test.go | 3 ++ eth/api_backend.go | 29 +++++++--- eth/api_backend_test.go | 73 ++++++++++++++++++++++++++ eth/backend.go | 12 +++-- eth/ethconfig/config.go | 4 ++ internal/ethapi/api.go | 2 +- internal/ethapi/backend.go | 8 +-- plugin/evm/config.go | 39 ++++++++------ plugin/evm/config_test.go | 7 +++ plugin/evm/vm.go | 1 + 10 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 eth/api_backend_test.go diff --git a/core/types/transaction_signing_test.go b/core/types/transaction_signing_test.go index 081b61b149..674173dc85 100644 --- a/core/types/transaction_signing_test.go +++ b/core/types/transaction_signing_test.go @@ -121,6 +121,9 @@ func TestEIP155SigningVitalik(t *testing.T) { if from != addr { t.Errorf("%d: expected %x got %x", i, addr, from) } + if !tx.Protected() { + t.Errorf("%d: expected to be protected", i) + } } } diff --git a/eth/api_backend.go b/eth/api_backend.go index 28e81aeafb..7c4e89da7b 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -54,10 +54,11 @@ var ErrUnfinalizedData = errors.New("cannot query unfinalized data") // EthAPIBackend implements ethapi.Backend for full nodes type EthAPIBackend struct { - extRPCEnabled bool - allowUnprotectedTxs bool - eth *Ethereum - gpo *gasprice.Oracle + extRPCEnabled bool + allowUnprotectedTxs bool + allowUnprotectedTxHashes map[common.Hash]struct{} // Invariant: read-only after creation. + eth *Ethereum + gpo *gasprice.Oracle } // ChainConfig returns the active chain configuration. @@ -391,8 +392,24 @@ func (b *EthAPIBackend) ExtRPCEnabled() bool { return b.extRPCEnabled } -func (b *EthAPIBackend) UnprotectedAllowed() bool { - return b.allowUnprotectedTxs +func (b *EthAPIBackend) UnprotectedAllowed(tx *types.Transaction) bool { + if b.allowUnprotectedTxs { + return true + } + + // Check for special cased transaction hashes: + // Note: this map is read-only after creation, so it is safe to read from it on multiple threads. + if _, ok := b.allowUnprotectedTxHashes[tx.Hash()]; ok { + return true + } + + // Check for "predictable pattern" (Nick's Signature: https://weka.medium.com/how-to-send-ether-to-11-440-people-187e332566b7) + v, r, s := tx.RawSignatureValues() + if v == nil || r == nil || s == nil { + return false + } + + return tx.Nonce() == 0 && r.Cmp(s) == 0 } func (b *EthAPIBackend) RPCGasCap() uint64 { diff --git a/eth/api_backend_test.go b/eth/api_backend_test.go new file mode 100644 index 0000000000..0d608f5085 --- /dev/null +++ b/eth/api_backend_test.go @@ -0,0 +1,73 @@ +// (c) 2019-2020, Ava Labs, Inc. +// +// This file is a derived work, based on the go-ethereum library whose original +// notices appear below. +// +// It is distributed under a license compatible with the licensing terms of the +// original code from which it is derived. +// +// Much love to the original authors for their work. +// ********** +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package eth + +import ( + "fmt" + "testing" + + "github.com/ava-labs/subnet-evm/core/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnprotectedAllowed(t *testing.T) { + allowedTxHashes := make(map[common.Hash]struct{}) + allowedTxHashes[common.HexToHash("0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c")] = struct{}{} // Special case EIP-2470 tx, since it's R and S values are not quite the same + + backend := &EthAPIBackend{ + allowUnprotectedTxs: false, + allowUnprotectedTxHashes: allowedTxHashes, + } + + for i, test := range []struct { + txRlp, addr string + }{ + {"f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222", "0x3fab184622dc19b6109349b94811493bf2a45362"}, // https://github.com/Arachnid/deterministic-deployment-proxy + {"f90a388085174876e800830c35008080b909e5608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c00291ba01820182018201820182018201820182018201820182018201820182018201820a01820182018201820182018201820182018201820182018201820182018201820", "0xa990077c3205cbDf861e17Fa532eeB069cE9fF96"}, // EIP-1820 https://eips.ethereum.org/EIPS/eip-1820 + {"f9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b83247000822470", "0xBb6e024b9cFFACB947A71991E386681B1Cd1477D"}, // https://github.com/status-im/EIPs/blob/singleton-factory/EIPS/eip-2470.md + } { + t.Run(fmt.Sprintf("nicks method %d", i), func(t *testing.T) { + signer := types.HomesteadSigner{} + + var tx *types.Transaction + err := rlp.DecodeBytes(common.Hex2Bytes(test.txRlp), &tx) + require.NoError(t, err) + + txHash := tx.Hash() + from, err := types.Sender(signer, tx) + require.NoError(t, err, "tx hash %s", txHash) + + addr := common.HexToAddress(test.addr) + assert.Equal(t, addr, from, "tx hash %s", txHash) + + assert.True(t, backend.UnprotectedAllowed(tx), "tx hash %s", txHash) + }) + } +} diff --git a/eth/backend.go b/eth/backend.go index d313817a2a..b0ce460f78 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -238,10 +238,16 @@ func New( eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, clock) + allowUnprotectedTxHashes := make(map[common.Hash]struct{}) + for _, txHash := range config.AllowUnprotectedTxHashes { + allowUnprotectedTxHashes[txHash] = struct{}{} + } + eth.APIBackend = &EthAPIBackend{ - extRPCEnabled: stack.Config().ExtRPCEnabled(), - allowUnprotectedTxs: config.AllowUnprotectedTxs, - eth: eth, + extRPCEnabled: stack.Config().ExtRPCEnabled(), + allowUnprotectedTxs: config.AllowUnprotectedTxs, + allowUnprotectedTxHashes: allowUnprotectedTxHashes, + eth: eth, } if config.AllowUnprotectedTxs { log.Info("Unprotected transactions allowed") diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index d28ba39677..bc29780228 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -32,6 +32,7 @@ import ( "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/eth/gasprice" "github.com/ava-labs/subnet-evm/miner" + "github.com/ethereum/go-ethereum/common" ) // DefaultFullGPOConfig contains default gasprice oracle settings for full node. @@ -132,6 +133,9 @@ type Config struct { // Unprotected transactions are transactions that are signed without EIP-155 // replay protection. AllowUnprotectedTxs bool + // AllowUnprotectedTxHashes provides a list of transaction hashes, which will be allowed + // to be issued without replay protection over the API even if AllowUnprotectedTxs is false. + AllowUnprotectedTxHashes []common.Hash // OfflinePruning enables offline pruning on startup of the node. If a node is started // with this configuration option, it must finish pruning before resuming normal operation. diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 11f2736759..ebf872015d 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1797,7 +1797,7 @@ func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c if err := checkTxFee(tx.GasPrice(), tx.Gas(), b.RPCTxFeeCap()); err != nil { return common.Hash{}, err } - if !b.UnprotectedAllowed() && !tx.Protected() { + if !b.UnprotectedAllowed(tx) && !tx.Protected() { // Ensure only eip155 signed transactions are submitted if EIP155Required is set. return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index c11cfa771b..ce09984381 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -58,10 +58,10 @@ type Backend interface { ChainDb() ethdb.Database AccountManager() *accounts.Manager ExtRPCEnabled() bool - RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection - RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection - RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs - UnprotectedAllowed() bool // allows only for EIP155 transactions. + RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection + RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection + RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs + UnprotectedAllowed(tx *types.Transaction) bool // allows only for EIP155 transactions. // Blockchain API HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) diff --git a/plugin/evm/config.go b/plugin/evm/config.go index 46bba20af2..a295690b3d 100644 --- a/plugin/evm/config.go +++ b/plugin/evm/config.go @@ -55,15 +55,20 @@ const ( defaultStateSyncMinBlocks = 300_000 ) -var defaultEnabledAPIs = []string{ - "eth", - "eth-filter", - "net", - "web3", - "internal-eth", - "internal-blockchain", - "internal-transaction", -} +var ( + defaultEnabledAPIs = []string{ + "eth", + "eth-filter", + "net", + "web3", + "internal-eth", + "internal-blockchain", + "internal-transaction", + } + defaultAllowUnprotectedTxHashes = []common.Hash{ + common.HexToHash("0xfefb2da535e927b85fe68eb81cb2e4a5827c905f78381a01ef2322aa9b0aee8e"), // EIP-1820: https://eips.ethereum.org/EIPS/eip-1820 + } +) type Duration struct { time.Duration @@ -114,13 +119,14 @@ type Config struct { MetricsExpensiveEnabled bool `json:"metrics-expensive-enabled"` // Debug-level metrics that might impact runtime performance // API Settings - LocalTxsEnabled bool `json:"local-txs-enabled"` - APIMaxDuration Duration `json:"api-max-duration"` - WSCPURefillRate Duration `json:"ws-cpu-refill-rate"` - WSCPUMaxStored Duration `json:"ws-cpu-max-stored"` - MaxBlocksPerRequest int64 `json:"api-max-blocks-per-request"` - AllowUnfinalizedQueries bool `json:"allow-unfinalized-queries"` - AllowUnprotectedTxs bool `json:"allow-unprotected-txs"` + LocalTxsEnabled bool `json:"local-txs-enabled"` + APIMaxDuration Duration `json:"api-max-duration"` + WSCPURefillRate Duration `json:"ws-cpu-refill-rate"` + WSCPUMaxStored Duration `json:"ws-cpu-max-stored"` + MaxBlocksPerRequest int64 `json:"api-max-blocks-per-request"` + AllowUnfinalizedQueries bool `json:"allow-unfinalized-queries"` + AllowUnprotectedTxs bool `json:"allow-unprotected-txs"` + AllowUnprotectedTxHashes []common.Hash `json:"allow-unprotected-tx-hashes"` // Keystore Settings KeystoreDirectory string `json:"keystore-directory"` // both absolute and relative supported @@ -209,6 +215,7 @@ func (c *Config) SetDefaults() { c.StateSyncServerTrieCache = defaultStateSyncServerTrieCache c.StateSyncCommitInterval = defaultSyncableCommitInterval c.StateSyncMinBlocks = defaultStateSyncMinBlocks + c.AllowUnprotectedTxHashes = defaultAllowUnprotectedTxHashes } func (d *Duration) UnmarshalJSON(data []byte) (err error) { diff --git a/plugin/evm/config_test.go b/plugin/evm/config_test.go index e81aa97ba6..0e067cb4b4 100644 --- a/plugin/evm/config_test.go +++ b/plugin/evm/config_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" ) @@ -56,6 +57,12 @@ func TestUnmarshalConfig(t *testing.T) { Config{StateSyncIDs: "NodeID-CaBYJ9kzHvrQFiYWowMkJGAQKGMJqZoat"}, false, }, + { + "allow unprotected tx hashes", + []byte(`{"allow-unprotected-tx-hashes": ["0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c"]}`), + Config{AllowUnprotectedTxHashes: []common.Hash{common.HexToHash("0x803351deb6d745e91545a6a3e1c0ea3e9a6a02a1a4193b70edfcd2f40f71a01c")}}, + false, + }, } for _, tt := range tests { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 6c2d39eef1..a999140d2d 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -307,6 +307,7 @@ func (vm *VM) Initialize( vm.ethConfig.TxPool.Locals = vm.config.PriorityRegossipAddresses vm.ethConfig.AllowUnfinalizedQueries = vm.config.AllowUnfinalizedQueries vm.ethConfig.AllowUnprotectedTxs = vm.config.AllowUnprotectedTxs + vm.ethConfig.AllowUnprotectedTxHashes = vm.config.AllowUnprotectedTxHashes vm.ethConfig.Preimages = vm.config.Preimages vm.ethConfig.Pruning = vm.config.Pruning vm.ethConfig.TrieCleanCache = vm.config.TrieCleanCache