From 67fc4d87fb5ab1687a3e6d5fef9585a65edf8908 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Wed, 13 Sep 2023 08:28:00 +0530 Subject: [PATCH] Added a new RPC endpoint (`bor_sendRawTransactionConditional`) to support EIP-4337 Bundled Transactions (#945) * added new api to support conditional transactions (EIP-4337) (#700) * Refactored the code and updated the miner to check for the validity of options (#793) * refactored the code and updated the miner to check for the validity of options * added new errors -32003 and -32005 * added unit tests * addressed comments * Aa 4337 update generics (#799) * poc * minor bug fix * use common.Hash * updated UnmarshalJSON function (reference - tynes) * fix * done * linters * with test * undo some unintentional changes --------- Co-authored-by: Pratik Patil * handelling the block range and timestamp range, also made timestamp a pointer --------- Co-authored-by: Evgeny Danilenko <6655321@bk.ru> * Added filtering of conditional transactions in txpool (#920) * added filtering of conditional transactions in txpool * minor fix in ValidateKnownAccounts * bug fix * Supporting nil knownAccounts * lints * bundled transactions are not announced/broadcasted to the peers * fixed after upstream merge * few fixes * sentry reject conditional transaction * Changed the namespace of conditional transaction API from `eth` to `bor` (#985) * added conditional transaction to bor namespace * test comit * test comit * added conditional transaction * namespapce changed to bor * cleanup * cleanup * addressed comments * reverted changes in ValidateKnownAccounts * addressed comments and removed unwanted code * addressed comments * bug fix * lint * removed licence from core/types/transaction_conditional_test.go --------- Co-authored-by: Evgeny Danilenko <6655321@bk.ru> --- common/types.go | 9 +- core/state/state_test.go | 66 +++++++++ core/state/statedb.go | 33 +++++ core/txpool/list.go | 27 ++++ core/txpool/list_test.go | 64 ++++++++ core/txpool/txpool.go | 13 +- core/txpool/txpool_test.go | 4 + core/types/block.go | 38 +++++ core/types/block_test.go | 164 +++++++++++++++++++++ core/types/transaction.go | 13 ++ core/types/transaction_conditional.go | 146 ++++++++++++++++++ core/types/transaction_conditional_test.go | 31 ++++ eth/api_backend.go | 4 + eth/backend.go | 1 + eth/protocols/eth/broadcast.go | 10 +- internal/ethapi/bor_api.go | 43 +++++- internal/jsre/deps/web3.js | 8 + internal/web3ext/bor_ext.go | 6 + miner/fake_miner.go | 2 +- miner/miner.go | 6 +- miner/test_backend.go | 10 +- miner/worker.go | 42 +++++- rpc/errors.go | 12 ++ rpc/handler.go | 1 + rpc/ipc.go | 1 + 25 files changed, 732 insertions(+), 22 deletions(-) create mode 100644 core/types/transaction_conditional.go create mode 100644 core/types/transaction_conditional_test.go diff --git a/common/types.go b/common/types.go index c0b903aca3cc1..d5ae9bc430da5 100644 --- a/common/types.go +++ b/common/types.go @@ -28,8 +28,9 @@ import ( "reflect" "strings" - "github.com/ethereum/go-ethereum/common/hexutil" "golang.org/x/crypto/sha3" + + "github.com/ethereum/go-ethereum/common/hexutil" ) // Lengths of hashes and addresses in bytes. @@ -66,6 +67,12 @@ func BigToHash(b *big.Int) Hash { return BytesToHash(b.Bytes()) } // If b is larger than len(h), b will be cropped from the left. func HexToHash(s string) Hash { return BytesToHash(FromHex(s)) } +func HexToRefHash(s string) *Hash { + v := BytesToHash(FromHex(s)) + + return &v +} + // Bytes gets the byte representation of the underlying hash. func (h Hash) Bytes() []byte { return h[:] } diff --git a/core/state/state_test.go b/core/state/state_test.go index a345d27719c18..3ccefdd1f94bc 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -21,8 +21,11 @@ import ( "math/big" "testing" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/trie" @@ -273,3 +276,66 @@ func compareStateObjects(so0, so1 *stateObject, t *testing.T) { } } } + +func TestValidateKnownAccounts(t *testing.T) { + t.Parallel() + + knownAccounts := make(types.KnownAccounts) + + types.InsertKnownAccounts(knownAccounts, common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add1"), common.HexToHash("0x2d6f8a898e7dec0bb7a50e8c142be32d7c98c096ff68ed57b9b08280d9aca1ce")) + types.InsertKnownAccounts(knownAccounts, common.HexToAddress("0xadd2add2add2add2add2add2add2add2add2add2"), map[common.Hash]common.Hash{ + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000aaa"): common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000bbb"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ccc"): common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ddd"), + }) + + stateobjaddr1 := common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add1") + stateobjaddr2 := common.HexToAddress("0xadd2add2add2add2add2add2add2add2add2add2") + + storageaddr1 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000zzz") + storageaddr21 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000aaa") + storageaddr22 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ccc") + + data1 := common.BytesToHash([]byte{24}) + data21 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000bbb") + data22 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ddd") + + s := newStateTest() + + // set initial state object value + s.state.SetState(stateobjaddr1, storageaddr1, data1) + s.state.SetState(stateobjaddr2, storageaddr21, data21) + s.state.SetState(stateobjaddr2, storageaddr22, data22) + + require.NoError(t, s.state.ValidateKnownAccounts(knownAccounts)) + + types.InsertKnownAccounts(knownAccounts, common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add2"), common.HexToHash("0x2d6f8a898e7dec0bb7a50e8c142be32d7c98c096ff68ed57b9b08280d9aca1cf")) + + stateobjaddr3 := common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add2") + storageaddr3 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000yyy") + data3 := common.BytesToHash([]byte{24}) + + s.state.SetState(stateobjaddr3, storageaddr3, data3) + + // expected error + err := s.state.ValidateKnownAccounts(knownAccounts) + require.Error(t, err, "should have been an error") + + // correct the previous mistake "0x2d6f8a898e7dec0bb7a50e8c142be32d7c98c096ff68ed57b9b08280d9aca1cf" -> "0x2d6f8a898e7dec0bb7a50e8c142be32d7c98c096ff68ed57b9b08280d9aca1ce" + types.InsertKnownAccounts(knownAccounts, common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add2"), common.HexToHash("0x2d6f8a898e7dec0bb7a50e8c142be32d7c98c096ff68ed57b9b08280d9aca1ce")) + types.InsertKnownAccounts(knownAccounts, common.HexToAddress("0xadd2add2add2add2add2add2add2add2add2add3"), map[common.Hash]common.Hash{ + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000aaa"): common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000bbb"), + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ccc"): common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ddd"), + }) + + stateobjaddr4 := common.HexToAddress("0xadd2add2add2add2add2add2add2add2add2add3") + storageaddr41 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000aaa") + storageaddr42 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000ccc") + data4 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000bbb") + + s.state.SetState(stateobjaddr4, storageaddr41, data4) + s.state.SetState(stateobjaddr4, storageaddr42, data4) + + // expected error + err = s.state.ValidateKnownAccounts(knownAccounts) + require.Error(t, err, "should have been an error") +} diff --git a/core/state/statedb.go b/core/state/statedb.go index d304ccb6b6203..9e680fae61868 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1640,6 +1640,39 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre return s.accessList.Contains(addr, slot) } +func (s *StateDB) ValidateKnownAccounts(knownAccounts types.KnownAccounts) error { + if knownAccounts == nil { + return nil + } + + for k, v := range knownAccounts { + // check if the value is hex string or an object + switch { + case v.IsSingle(): + trie, _ := s.StorageTrie(k) + if trie != nil { + actualRootHash := trie.Hash() + if *v.Single != actualRootHash { + return fmt.Errorf("invalid root hash for: %v root hash: %v actual root hash: %v", k, v.Single, actualRootHash) + } + } else { + return fmt.Errorf("Storage Trie is nil for: %v", k) + } + case v.IsStorage(): + for slot, value := range v.Storage { + actualValue := s.GetState(k, slot) + if value != actualValue { + return fmt.Errorf("invalid slot value at address: %v slot: %v value: %v actual value: %v", k, slot, value, actualValue) + } + } + default: + return fmt.Errorf("impossible to validate known accounts: %v", k) + } + } + + return nil +} + // convertAccountSet converts a provided account set from address keyed to hash keyed. func (s *StateDB) convertAccountSet(set map[common.Address]struct{}) map[common.Hash]struct{} { ret := make(map[common.Hash]struct{}) diff --git a/core/txpool/list.go b/core/txpool/list.go index dd4e28b54c938..2435de5c38bfd 100644 --- a/core/txpool/list.go +++ b/core/txpool/list.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/common" cmath "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -563,6 +564,32 @@ func (l *list) Filter(costLimit *uint256.Int, gasLimit uint64) (types.Transactio return removed, invalids } +// FilterTxConditional returns the conditional transactions with invalid KnownAccounts +// TODO - We will also have to check block range and time stamp range! +func (l *list) FilterTxConditional(state *state.StateDB) types.Transactions { + removed := l.txs.filter(func(tx *types.Transaction) bool { + if options := tx.GetOptions(); options != nil { + err := state.ValidateKnownAccounts(options.KnownAccounts) + if err != nil { + log.Error("Error while Filtering Tx Conditional", "err", err) + return true + } + + return false + } + + return false + }) + + if len(removed) == 0 { + return nil + } + + l.txs.reheap(true) + + return removed +} + // Cap places a hard limit on the number of items, returning all transactions // exceeding that limit. func (l *list) Cap(threshold int) types.Transactions { diff --git a/core/txpool/list_test.go b/core/txpool/list_test.go index 11032e09bb862..5f12b2fdb6d81 100644 --- a/core/txpool/list_test.go +++ b/core/txpool/list_test.go @@ -22,7 +22,11 @@ import ( "testing" "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" ) @@ -78,3 +82,63 @@ func BenchmarkListAdd(b *testing.B) { } } } + +func TestFilterTxConditional(t *testing.T) { + t.Parallel() + + // Create an in memory state db to test against. + memDb := rawdb.NewMemoryDatabase() + db := state.NewDatabase(memDb) + state, _ := state.New(common.Hash{}, db, nil) + + // Create a private key to sign transactions. + key, _ := crypto.GenerateKey() + + // Create a list. + list := newList(true) + + // Create a transaction with no defined tx options + // and add to the list. + tx := transaction(0, 1000, key) + list.Add(tx, DefaultConfig.PriceBump) + + // There should be no drops at this point. + // No state has been modified. + drops := list.FilterTxConditional(state) + + count := len(drops) + require.Equal(t, 0, count, "got %d filtered by TxOptions when there should not be any", count) + + // Create another transaction with a known account storage root tx option + // and add to the list. + tx2 := transaction(1, 1000, key) + + var options types.OptionsAA4337 + + options.KnownAccounts = types.KnownAccounts{ + common.Address{19: 1}: &types.Value{ + Single: common.HexToRefHash("0xe734938daf39aae1fa4ee64dc3155d7c049f28b57a8ada8ad9e86832e0253bef"), + }, + } + + state.SetState(common.Address{19: 1}, common.Hash{}, common.Hash{30: 1}) + tx2.PutOptions(&options) + list.Add(tx2, DefaultConfig.PriceBump) + + // There should still be no drops as no state has been modified. + drops = list.FilterTxConditional(state) + + count = len(drops) + require.Equal(t, 0, count, "got %d filtered by TxOptions when there should not be any", count) + + // Set state that conflicts with tx2's policy + state.SetState(common.Address{19: 1}, common.Hash{}, common.Hash{31: 1}) + + // tx2 should be the single transaction filtered out + drops = list.FilterTxConditional(state) + + count = len(drops) + require.Equal(t, 1, count, "got %d filtered by TxOptions when there should be a single one", count) + + require.Equal(t, tx2, drops[0], "Got %x, expected %x", drops[0].Hash(), tx2.Hash()) +} diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 9ced7c630d998..9da20cd76356b 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -2303,10 +2303,19 @@ func (pool *TxPool) demoteUnexecutables() { pool.enqueueTx(hash, tx, false, false) } - pendingGauge.Dec(int64(oldsLen + dropsLen + invalidsLen)) + // Drop all transactions that no longer have valid TxOptions + txConditionalsRemoved := list.FilterTxConditional(pool.currentState) + + for _, tx := range txConditionalsRemoved { + hash := tx.Hash() + pool.all.Remove(hash) + log.Trace("Removed invalid conditional transaction", "hash", hash) + } + + pendingGauge.Dec(int64(oldsLen + dropsLen + invalidsLen + len(txConditionalsRemoved))) if pool.locals.contains(addr) { - localGauge.Dec(int64(oldsLen + dropsLen + invalidsLen)) + localGauge.Dec(int64(oldsLen + dropsLen + invalidsLen + len(txConditionalsRemoved))) } // If there's a gap in front, alert (should never happen) and postpone all transactions if list.Len() > 0 && list.txs.Get(nonce) == nil { diff --git a/core/txpool/txpool_test.go b/core/txpool/txpool_test.go index 8150c9174898f..c8c201b193d62 100644 --- a/core/txpool/txpool_test.go +++ b/core/txpool/txpool_test.go @@ -1993,6 +1993,7 @@ func TestUnderpricing(t *testing.T) { keys[i], _ = crypto.GenerateKey() testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) } + // Generate and queue a batch of transactions, both pending and queued txs := types.Transactions{} @@ -2023,6 +2024,7 @@ func TestUnderpricing(t *testing.T) { if err := validatePoolInternals(pool); err != nil { t.Fatalf("pool internal state corrupted: %v", err) } + // Ensure that adding an underpriced transaction on block limit fails if err := pool.AddRemote(pricedTransaction(0, 100000, big.NewInt(1), keys[1])); !errors.Is(err, ErrUnderpriced) { t.Fatalf("adding underpriced pending transaction error mismatch: have %v, want %v", err, ErrUnderpriced) @@ -2064,6 +2066,7 @@ func TestUnderpricing(t *testing.T) { if err := validatePoolInternals(pool); err != nil { t.Fatalf("pool internal state corrupted: %v", err) } + // Ensure that adding local transactions can push out even higher priced ones ltx = pricedTransaction(1, 100000, big.NewInt(0), keys[2]) if err := pool.AddLocal(ltx); err != nil { @@ -2263,6 +2266,7 @@ func TestUnderpricingDynamicFee(t *testing.T) { if err := validatePoolInternals(pool); err != nil { t.Fatalf("pool internal state corrupted: %v", err) } + // Ensure that adding local transactions can push out even higher priced ones ltx = dynamicFeeTx(1, 100000, big.NewInt(0), big.NewInt(0), keys[2]) if err := pool.AddLocal(ltx); err != nil { diff --git a/core/types/block.go b/core/types/block.go index ee78aa6717c2e..e732e0a25f4c2 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -188,6 +188,44 @@ func (h *Header) EmptyReceipts() bool { return h.ReceiptHash == EmptyReceiptsHash } +// ValidateBlockNumberOptions4337 validates the block range passed as in the options parameter in the conditional transaction (EIP-4337) +func (h *Header) ValidateBlockNumberOptions4337(minBlockNumber *big.Int, maxBlockNumber *big.Int) error { + currentBlockNumber := h.Number + + if minBlockNumber != nil { + if currentBlockNumber.Cmp(minBlockNumber) == -1 { + return fmt.Errorf("current block number %v is less than minimum block number: %v", currentBlockNumber, minBlockNumber) + } + } + + if maxBlockNumber != nil { + if currentBlockNumber.Cmp(maxBlockNumber) == 1 { + return fmt.Errorf("current block number %v is greater than maximum block number: %v", currentBlockNumber, maxBlockNumber) + } + } + + return nil +} + +// ValidateBlockNumberOptions4337 validates the timestamp range passed as in the options parameter in the conditional transaction (EIP-4337) +func (h *Header) ValidateTimestampOptions4337(minTimestamp *uint64, maxTimestamp *uint64) error { + currentBlockTime := h.Time + + if minTimestamp != nil { + if currentBlockTime < *minTimestamp { + return fmt.Errorf("current block time %v is less than minimum timestamp: %v", currentBlockTime, minTimestamp) + } + } + + if maxTimestamp != nil { + if currentBlockTime > *maxTimestamp { + return fmt.Errorf("current block time %v is greater than maximum timestamp: %v", currentBlockTime, maxTimestamp) + } + } + + return nil +} + // Body is a simple (mutable, non-safe) data container for storing and moving // a block's data contents (transactions and uncles) together. type Body struct { diff --git a/core/types/block_test.go b/core/types/block_test.go index 74901fcf33b8c..aed203b31e157 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -463,3 +463,167 @@ func TestRlpDecodeParentHash(t *testing.T) { } } } + +func TestValidateBlockNumberOptions4337(t *testing.T) { + t.Parallel() + + testsPass := []struct { + number string + header Header + minBlockNumber *big.Int + maxBlockNumber *big.Int + }{ + { + "1", + Header{Number: big.NewInt(10)}, + big.NewInt(0), + big.NewInt(20), + }, + { + "2", + Header{Number: big.NewInt(10)}, + big.NewInt(10), + big.NewInt(10), + }, + { + "3", + Header{Number: big.NewInt(10)}, + big.NewInt(10), + big.NewInt(11), + }, + { + "4", + Header{Number: big.NewInt(10)}, + big.NewInt(0), + big.NewInt(10), + }, + } + + testsFail := []struct { + number string + header Header + minBlockNumber *big.Int + maxBlockNumber *big.Int + }{ + { + "5", + Header{Number: big.NewInt(10)}, + big.NewInt(0), + big.NewInt(0), + }, + { + "6", + Header{Number: big.NewInt(10)}, + big.NewInt(0), + big.NewInt(9), + }, + { + "7", + Header{Number: big.NewInt(10)}, + big.NewInt(11), + big.NewInt(9), + }, + { + "8", + Header{Number: big.NewInt(10)}, + big.NewInt(11), + big.NewInt(20), + }, + } + + for _, test := range testsPass { + if err := test.header.ValidateBlockNumberOptions4337(test.minBlockNumber, test.maxBlockNumber); err != nil { + t.Fatalf("test number %v should not have failed. err: %v", test.number, err) + } + } + + for _, test := range testsFail { + if err := test.header.ValidateBlockNumberOptions4337(test.minBlockNumber, test.maxBlockNumber); err == nil { + t.Fatalf("test number %v should have failed. err is nil", test.number) + } + } +} + +func TestValidateTimestampOptions4337(t *testing.T) { + t.Parallel() + + u64Ptr := func(n uint64) *uint64 { + return &n + } + + testsPass := []struct { + number string + header Header + minTimestamp *uint64 + maxTimestamp *uint64 + }{ + { + "1", + Header{Time: 1600000000}, + u64Ptr(1500000000), + u64Ptr(1700000000), + }, + { + "2", + Header{Time: 1600000000}, + u64Ptr(1600000000), + u64Ptr(1600000000), + }, + { + "3", + Header{Time: 1600000000}, + u64Ptr(1600000000), + u64Ptr(1700000000), + }, + { + "4", + Header{Time: 1600000000}, + u64Ptr(1500000000), + u64Ptr(1600000000), + }, + } + + testsFail := []struct { + number string + header Header + minTimestamp *uint64 + maxTimestamp *uint64 + }{ + { + "5", + Header{Time: 1600000000}, + u64Ptr(1500000000), + u64Ptr(1500000000), + }, + { + "6", + Header{Time: 1600000000}, + u64Ptr(1400000000), + u64Ptr(1500000000), + }, + { + "7", + Header{Time: 1600000000}, + u64Ptr(1700000000), + u64Ptr(1500000000), + }, + { + "8", + Header{Time: 1600000000}, + u64Ptr(1700000000), + u64Ptr(1800000000), + }, + } + + for _, test := range testsPass { + if err := test.header.ValidateTimestampOptions4337(test.minTimestamp, test.maxTimestamp); err != nil { + t.Fatalf("test number %v should not have failed. err: %v", test.number, err) + } + } + + for _, test := range testsFail { + if err := test.header.ValidateTimestampOptions4337(test.minTimestamp, test.maxTimestamp); err == nil { + t.Fatalf("test number %v should have failed. err is nil", test.number) + } + } +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 13062b94f9ad2..b16f6fc3ba954 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -54,6 +54,9 @@ type Transaction struct { inner TxData // Consensus contents of a transaction time time.Time // Time first seen locally (spam avoidance) + // knownAccounts (EIP-4337) + optionsAA4337 *OptionsAA4337 + // caches hash atomic.Pointer[common.Hash] size atomic.Pointer[uint64] @@ -101,6 +104,16 @@ type TxData interface { effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int } +// PutOptions stores the optionsAA4337 field of the conditional transaction (EIP-4337) +func (tx *Transaction) PutOptions(options *OptionsAA4337) { + tx.optionsAA4337 = options +} + +// GetOptions returns the optionsAA4337 field of the conditional transaction (EIP-4337) +func (tx *Transaction) GetOptions() *OptionsAA4337 { + return tx.optionsAA4337 +} + // EncodeRLP implements rlp.Encoder func (tx *Transaction) EncodeRLP(w io.Writer) error { if tx.Type() == LegacyTxType { diff --git a/core/types/transaction_conditional.go b/core/types/transaction_conditional.go new file mode 100644 index 0000000000000..358303a0b5d02 --- /dev/null +++ b/core/types/transaction_conditional.go @@ -0,0 +1,146 @@ +package types + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type KnownAccounts map[common.Address]*Value + +type Value struct { + Single *common.Hash + Storage map[common.Hash]common.Hash +} + +func SingleFromHex(hex string) *Value { + return &Value{Single: common.HexToRefHash(hex)} +} + +func FromMap(m map[string]string) *Value { + res := map[common.Hash]common.Hash{} + + for k, v := range m { + res[common.HexToHash(k)] = common.HexToHash(v) + } + + return &Value{Storage: res} +} + +func (v *Value) IsSingle() bool { + return v != nil && v.Single != nil && !v.IsStorage() +} + +func (v *Value) IsStorage() bool { + return v != nil && v.Storage != nil +} + +const EmptyValue = "{}" + +func (v *Value) MarshalJSON() ([]byte, error) { + if v.IsSingle() { + return json.Marshal(v.Single) + } + + if v.IsStorage() { + return json.Marshal(v.Storage) + } + + return []byte(EmptyValue), nil +} + +const hashTypeName = "Hash" + +func (v *Value) UnmarshalJSON(data []byte) error { + if len(data) == 0 { + return nil + } + + var m map[string]json.RawMessage + + err := json.Unmarshal(data, &m) + if err != nil { + // single Hash value case + v.Single = new(common.Hash) + + innerErr := json.Unmarshal(data, v.Single) + if innerErr != nil { + return fmt.Errorf("can't unmarshal to single value with error: %v value %q", innerErr, string(data)) + } + + return nil + } + + res := make(map[common.Hash]common.Hash, len(m)) + + for k, v := range m { + // check k if it is a Hex value + var kHash common.Hash + + err = hexutil.UnmarshalFixedText(hashTypeName, []byte(k), kHash[:]) + if err != nil { + return fmt.Errorf("%w by key: %s with key %q and value %q", ErrKnownAccounts, err, k, string(v)) + } + + // check v if it is a Hex value + var vHash common.Hash + + err = hexutil.UnmarshalFixedText("hashTypeName", bytes.Trim(v, "\""), vHash[:]) + if err != nil { + return fmt.Errorf("%w by value: %s with key %q and value %q", ErrKnownAccounts, err, k, string(v)) + } + + res[kHash] = vHash + } + + v.Storage = res + + return nil +} + +func InsertKnownAccounts[T common.Hash | map[common.Hash]common.Hash](accounts KnownAccounts, k common.Address, v T) { + switch typedV := any(v).(type) { + case common.Hash: + accounts[k] = &Value{Single: &typedV} + case map[common.Hash]common.Hash: + accounts[k] = &Value{Storage: typedV} + } +} + +type OptionsAA4337 struct { + KnownAccounts KnownAccounts `json:"knownAccounts"` + BlockNumberMin *big.Int `json:"blockNumberMin"` + BlockNumberMax *big.Int `json:"blockNumberMax"` + TimestampMin *uint64 `json:"timestampMin"` + TimestampMax *uint64 `json:"timestampMax"` +} + +var ErrKnownAccounts = errors.New("an incorrect list of knownAccounts") + +func (ka KnownAccounts) ValidateLength() error { + if ka == nil { + return nil + } + + length := 0 + + for _, v := range ka { + // check if the value is hex string or an object + if v.IsSingle() { + length += 1 + } else { + length += len(v.Storage) + } + } + + if length >= 1000 { + return fmt.Errorf("number of slots/accounts in KnownAccounts %v exceeds the limit of 1000", length) + } + + return nil +} diff --git a/core/types/transaction_conditional_test.go b/core/types/transaction_conditional_test.go new file mode 100644 index 0000000000000..03ce473d16644 --- /dev/null +++ b/core/types/transaction_conditional_test.go @@ -0,0 +1,31 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +func TestKnownAccounts(t *testing.T) { + t.Parallel() + + requestRaw := []byte(`{"0xadd1add1add1add1add1add1add1add1add1add1": "0x000000000000000000000000313aadca1750caadc7bcb26ff08175c95dcf8e38", "0xadd2add2add2add2add2add2add2add2add2add2": {"0x0000000000000000000000000000000000000000000000000000000000000aaa": "0x0000000000000000000000000000000000000000000000000000000000000bbb", "0x0000000000000000000000000000000000000000000000000000000000000ccc": "0x0000000000000000000000000000000000000000000000000000000000000ddd"}}`) + + accs := &KnownAccounts{} + + err := json.Unmarshal(requestRaw, accs) + require.NoError(t, err) + + expected := &KnownAccounts{ + common.HexToAddress("0xadd1add1add1add1add1add1add1add1add1add1"): SingleFromHex("0x000000000000000000000000313aadca1750caadc7bcb26ff08175c95dcf8e38"), + common.HexToAddress("0xadd2add2add2add2add2add2add2add2add2add2"): FromMap(map[string]string{ + "0x0000000000000000000000000000000000000000000000000000000000000aaa": "0x0000000000000000000000000000000000000000000000000000000000000bbb", + "0x0000000000000000000000000000000000000000000000000000000000000ccc": "0x0000000000000000000000000000000000000000000000000000000000000ddd", + }), + } + + require.Equal(t, expected, accs) +} diff --git a/eth/api_backend.go b/eth/api_backend.go index 080ff0dc3e923..a6f644943587e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -313,6 +313,10 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri } func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { + if signedTx.GetOptions() != nil && !b.eth.Miner().GetWorker().IsRunning() { + return errors.New("bundled transactions are not broadcasted therefore they will not submitted to the transaction pool") + } + err := b.eth.txPool.AddLocal(signedTx) if err != nil { if unwrapped := errors.Unwrap(err); unwrapped != nil { diff --git a/eth/backend.go b/eth/backend.go index 061edb0c065d2..2f64f1671fcf3 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -361,6 +361,7 @@ func makeExtraData(extra []byte) []byte { return extra } +// PeerCount returns the number of connected peers. func (s *Ethereum) PeerCount() int { return s.p2pServer.PeerCount() } diff --git a/eth/protocols/eth/broadcast.go b/eth/protocols/eth/broadcast.go index 45844a66c3779..9d18869d35506 100644 --- a/eth/protocols/eth/broadcast.go +++ b/eth/protocols/eth/broadcast.go @@ -84,7 +84,10 @@ func (p *Peer) broadcastTransactions() { ) for i := 0; i < len(queue) && size < maxTxPacketSize; i++ { - if tx := p.txpool.Get(queue[i]); tx != nil { + tx := p.txpool.Get(queue[i]) + + // Skip EIP-4337 bundled transactions + if tx != nil && tx.GetOptions() == nil { txs = append(txs, tx) size += common.StorageSize(tx.Size()) } @@ -158,7 +161,10 @@ func (p *Peer) announceTransactions() { ) for count = 0; count < len(queue) && size < maxTxPacketSize; count++ { - if tx := p.txpool.Get(queue[count]); tx != nil { + tx := p.txpool.Get(queue[count]) + + // Skip EIP-4337 bundled transactions + if tx != nil && tx.GetOptions() == nil { pending = append(pending, queue[count]) pendingTypes = append(pendingTypes, tx.Type()) pendingSizes = append(pendingSizes, uint32(tx.Size())) diff --git a/internal/ethapi/bor_api.go b/internal/ethapi/bor_api.go index 289f6a3650cb8..d193bd6329cb3 100644 --- a/internal/ethapi/bor_api.go +++ b/internal/ethapi/bor_api.go @@ -4,7 +4,9 @@ import ( "context" "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/rpc" ) // GetRootHash returns root hash for given start and end block @@ -52,16 +54,53 @@ func (s *BlockChainAPI) appendRPCMarshalBorTransaction(ctx context.Context, bloc return fields } -// EthereumAPI provides an API to access Ethereum related information. +// BorAPI provides an API to access Bor related information. type BorAPI struct { b Backend } -// NewEthereumAPI creates a new Ethereum protocol API. +// NewBorAPI creates a new Bor protocol API. func NewBorAPI(b Backend) *BorAPI { return &BorAPI{b} } +// SendRawTransactionConditional will add the signed transaction to the transaction pool. +// The sender/bundler is responsible for signing the transaction +func (api *BorAPI) SendRawTransactionConditional(ctx context.Context, input hexutil.Bytes, options types.OptionsAA4337) (common.Hash, error) { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(input); err != nil { + return common.Hash{}, err + } + + currentHeader := api.b.CurrentHeader() + currentState, _, _ := api.b.StateAndHeaderByNumber(ctx, rpc.BlockNumber(currentHeader.Number.Int64())) + + // check block number range + if err := currentHeader.ValidateBlockNumberOptions4337(options.BlockNumberMin, options.BlockNumberMax); err != nil { + return common.Hash{}, &rpc.OptionsValidateError{Message: "out of block range. err: " + err.Error()} + } + + // check timestamp range + if err := currentHeader.ValidateTimestampOptions4337(options.TimestampMin, options.TimestampMax); err != nil { + return common.Hash{}, &rpc.OptionsValidateError{Message: "out of time range. err: " + err.Error()} + } + + // check knownAccounts length (number of slots/accounts) should be less than 1000 + if err := options.KnownAccounts.ValidateLength(); err != nil { + return common.Hash{}, &rpc.KnownAccountsLimitExceededError{Message: "limit exceeded. err: " + err.Error()} + } + + // check knownAccounts + if err := currentState.ValidateKnownAccounts(options.KnownAccounts); err != nil { + return common.Hash{}, &rpc.OptionsValidateError{Message: "storage error. err: " + err.Error()} + } + + // put options data in Tx, to use it later while block building + tx.PutOptions(&options) + + return SubmitTransaction(ctx, api.b, tx) +} + func (api *BorAPI) GetVoteOnHash(ctx context.Context, starBlockNr uint64, endBlockNr uint64, hash string, milestoneId string) (bool, error) { return api.b.GetVoteOnHash(ctx, starBlockNr, endBlockNr, hash, milestoneId) } diff --git a/internal/jsre/deps/web3.js b/internal/jsre/deps/web3.js index 7d3d6d163d14c..fe6a17fefc899 100644 --- a/internal/jsre/deps/web3.js +++ b/internal/jsre/deps/web3.js @@ -5383,6 +5383,13 @@ var methods = function () { inputFormatter: [null] }); + var sendRawTransactionConditional = new Method({ + name: 'sendRawTransactionConditional', + call: 'eth_sendRawTransactionConditional', + params: 2, + inputFormatter: [null] + }); + var sendTransaction = new Method({ name: 'sendTransaction', call: 'eth_sendTransaction', @@ -5471,6 +5478,7 @@ var methods = function () { call, estimateGas, sendRawTransaction, + sendRawTransactionConditional, signTransaction, sendTransaction, sign, diff --git a/internal/web3ext/bor_ext.go b/internal/web3ext/bor_ext.go index 2f2f2acdbc32c..c8236dbcac98d 100644 --- a/internal/web3ext/bor_ext.go +++ b/internal/web3ext/bor_ext.go @@ -65,6 +65,12 @@ web3._extend({ call: 'bor_getVoteOnHash', params: 4, }), + new web3._extend.Method({ + name: 'sendRawTransactionConditional', + call: 'bor_sendRawTransactionConditional', + params: 2, + inputFormatter: [null] + }), ] }); ` diff --git a/miner/fake_miner.go b/miner/fake_miner.go index b3b58c0dc2c3d..ed7b3d9ac87d1 100644 --- a/miner/fake_miner.go +++ b/miner/fake_miner.go @@ -172,7 +172,7 @@ type mockBackend struct { txPool *txpool.TxPool } -// PeerCount implements Backend. +// PeerCount implements mockBackend. func (*mockBackend) PeerCount() int { panic("unimplemented") } diff --git a/miner/miner.go b/miner/miner.go index 3735f0ac6db95..b55ba984819ad 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -104,6 +104,10 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even return miner } +func (miner *Miner) GetWorker() *worker { + return miner.worker +} + // update keeps track of the downloader events. Please be aware that this is a one shot type of update loop. // It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and // the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks @@ -191,7 +195,7 @@ func (miner *Miner) Close() { } func (miner *Miner) Mining() bool { - return miner.worker.isRunning() + return miner.worker.IsRunning() } func (miner *Miner) Hashrate() uint64 { diff --git a/miner/test_backend.go b/miner/test_backend.go index 67e9f53e949b6..bb63f1b56231d 100644 --- a/miner/test_backend.go +++ b/miner/test_backend.go @@ -95,7 +95,7 @@ type testWorkerBackend struct { uncleBlock *types.Block } -// PeerCount implements Backend. +// PeerCount implements testWorkerBackend. func (*testWorkerBackend) PeerCount() int { panic("unimplemented") } @@ -397,7 +397,7 @@ func (w *worker) mainLoopWithDelay(ctx context.Context, delay uint, opcodeDelay // If our sealing block contains less than 2 uncle blocks, // add the new uncle block if valid and regenerate a new // sealing block for higher profit. - if w.isRunning() && w.current != nil && len(w.current.uncles) < 2 { + if w.IsRunning() && w.current != nil && len(w.current.uncles) < 2 { start := time.Now() if err := w.commitUncle(w.current, ev.Block.Header()); err == nil { commitErr := w.commit(ctx, w.current.copy(), nil, true, start) @@ -428,7 +428,7 @@ func (w *worker) mainLoopWithDelay(ctx context.Context, delay uint, opcodeDelay // already included in the current sealing block. These transactions will // be automatically eliminated. // nolint : nestif - if !w.isRunning() && w.current != nil { + if !w.IsRunning() && w.current != nil { // If block is already full, abort if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas { continue @@ -486,7 +486,7 @@ func (w *worker) commitWorkWithDelay(ctx context.Context, interrupt *int32, noem tracing.Exec(ctx, "", "worker.prepareWork", func(ctx context.Context, span trace.Span) { // Set the coinbase if the worker is running or it's required var coinbase common.Address - if w.isRunning() { + if w.IsRunning() { if w.coinbase == (common.Address{}) { log.Error("Refusing to mine without etherbase") return @@ -873,7 +873,7 @@ mainloop: } } - if !w.isRunning() && len(coalescedLogs) > 0 { + if !w.IsRunning() && len(coalescedLogs) > 0 { // We don't push the pendingLogsEvent while we are sealing. The reason is that // when we are sealing, the worker will regenerate a sealing block every 3 seconds. // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing. diff --git a/miner/worker.go b/miner/worker.go index c74a0aff62aa8..da27492847e6a 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -487,7 +487,7 @@ func (w *worker) stop() { } // isRunning returns an indicator whether worker is running or not. -func (w *worker) isRunning() bool { +func (w *worker) IsRunning() bool { return w.running.Load() } @@ -589,7 +589,7 @@ func (w *worker) newWorkLoop(ctx context.Context, recommit time.Duration) { case <-timer.C: // If sealing is running resubmit a new work cycle periodically to pull in // higher priced transactions. Disable this overhead for pending blocks. - if w.isRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { + if w.IsRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { // Short circuit if no new transaction arrives. if w.newTxs.Load() == 0 { timer.Reset(recommit) @@ -691,7 +691,7 @@ func (w *worker) mainLoop(ctx context.Context) { } // If our mining block contains less than 2 uncle blocks, // add the new uncle block if valid and regenerate a mining block. - if w.isRunning() && w.current != nil && len(w.current.uncles) < 2 { + if w.IsRunning() && w.current != nil && len(w.current.uncles) < 2 { start := time.Now() if err := w.commitUncle(w.current, ev.Block.Header()); err == nil { commitErr := w.commit(ctx, w.current.copy(), nil, true, start) @@ -721,7 +721,8 @@ func (w *worker) mainLoop(ctx context.Context) { // Note all transactions received may not be continuous with transactions // already included in the current sealing block. These transactions will // be automatically eliminated. - if !w.isRunning() && w.current != nil { + // nolint : nestif + if !w.IsRunning() && w.current != nil { // If block is already full, abort if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas { continue @@ -1152,6 +1153,31 @@ mainloop: // during transaction acceptance is the transaction pool. from, _ := types.Sender(env.signer, tx) + // not prioritising conditional transaction, yet. + //nolint:nestif + if options := tx.GetOptions(); options != nil { + if err := env.header.ValidateBlockNumberOptions4337(options.BlockNumberMin, options.BlockNumberMax); err != nil { + log.Trace("Dropping conditional transaction", "from", from, "hash", tx.Hash(), "reason", err) + txs.Pop() + + continue + } + + if err := env.header.ValidateTimestampOptions4337(options.TimestampMin, options.TimestampMax); err != nil { + log.Trace("Dropping conditional transaction", "from", from, "hash", tx.Hash(), "reason", err) + txs.Pop() + + continue + } + + if err := env.state.ValidateKnownAccounts(options.KnownAccounts); err != nil { + log.Trace("Dropping conditional transaction", "from", from, "hash", tx.Hash(), "reason", err) + txs.Pop() + + continue + } + } + // Check whether the tx is replay protected. If we're not in the EIP155 hf // phase, start ignoring the sender until we do. if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) { @@ -1217,7 +1243,7 @@ mainloop: } // nolint:nestif - if EnableMVHashMap && w.isRunning() { + if EnableMVHashMap && w.IsRunning() { close(chDeps) depsWg.Wait() @@ -1278,7 +1304,7 @@ mainloop: } - if !w.isRunning() && len(coalescedLogs) > 0 { + if !w.IsRunning() && len(coalescedLogs) > 0 { // We don't push the pendingLogsEvent while we are sealing. The reason is that // when we are sealing, the worker will regenerate a sealing block every 3 seconds. // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing. @@ -1692,7 +1718,7 @@ func (w *worker) commitWork(ctx context.Context, interrupt *atomic.Int32, noempt tracing.Exec(ctx, "", "worker.prepareWork", func(ctx context.Context, span trace.Span) { // Set the coinbase if the worker is running or it's required var coinbase common.Address - if w.isRunning() { + if w.IsRunning() { coinbase = w.etherbase() if coinbase == (common.Address{}) { log.Error("Refusing to mine without etherbase") @@ -1807,7 +1833,7 @@ func getInterruptTimer(ctx context.Context, work *environment, current *types.Bl // Note the assumption is held that the mutation is allowed to the passed env, do // the deep copy first. func (w *worker) commit(ctx context.Context, env *environment, interval func(), update bool, start time.Time) error { - if w.isRunning() { + if w.IsRunning() { ctx, span := tracing.StartSpan(ctx, "commit") defer tracing.EndSpan(span) diff --git a/rpc/errors.go b/rpc/errors.go index f1e90d26e1359..29a72c82c2f15 100644 --- a/rpc/errors.go +++ b/rpc/errors.go @@ -133,3 +133,15 @@ type CustomError struct { func (e *CustomError) ErrorCode() int { return e.Code } func (e *CustomError) Error() string { return e.ValidationError } + +type OptionsValidateError struct{ Message string } + +func (e *OptionsValidateError) ErrorCode() int { return -32003 } + +func (e *OptionsValidateError) Error() string { return e.Message } + +type KnownAccountsLimitExceededError struct{ Message string } + +func (e *KnownAccountsLimitExceededError) ErrorCode() int { return -32005 } + +func (e *KnownAccountsLimitExceededError) Error() string { return e.Message } diff --git a/rpc/handler.go b/rpc/handler.go index f126bf973e916..a056176b8b41a 100644 --- a/rpc/handler.go +++ b/rpc/handler.go @@ -375,6 +375,7 @@ func (h *handler) startCallProc(fn func(*callProc)) { h.executionPool.Submit(context.Background(), func() error { defer h.callWG.Done() defer cancel() + fn(&callProc{ctx: ctx}) h.executionPool.processed.Add(1) diff --git a/rpc/ipc.go b/rpc/ipc.go index c2cd4917a221f..66028b29f5067 100644 --- a/rpc/ipc.go +++ b/rpc/ipc.go @@ -30,6 +30,7 @@ func (s *Server) ServeListener(l net.Listener) error { conn, err := l.Accept() if netutil.IsTemporaryError(err) { log.Warn("RPC accept error", "err", err) + continue } else if err != nil { return err