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 <pratikspatil024@gmail.com>

* 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>
This commit is contained in:
Pratik Patil 2023-09-13 08:28:00 +05:30 committed by GitHub
parent 3442c0336d
commit 67fc4d87fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 732 additions and 22 deletions

View file

@ -28,8 +28,9 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"github.com/ethereum/go-ethereum/common/hexutil"
) )
// Lengths of hashes and addresses in bytes. // 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. // If b is larger than len(h), b will be cropped from the left.
func HexToHash(s string) Hash { return BytesToHash(FromHex(s)) } 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. // Bytes gets the byte representation of the underlying hash.
func (h Hash) Bytes() []byte { return h[:] } func (h Hash) Bytes() []byte { return h[:] }

View file

@ -21,8 +21,11 @@ import (
"math/big" "math/big"
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/trie" "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")
}

View file

@ -1640,6 +1640,39 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre
return s.accessList.Contains(addr, slot) 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. // 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{} { func (s *StateDB) convertAccountSet(set map[common.Address]struct{}) map[common.Hash]struct{} {
ret := make(map[common.Hash]struct{}) ret := make(map[common.Hash]struct{})

View file

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
cmath "github.com/ethereum/go-ethereum/common/math" 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/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -563,6 +564,32 @@ func (l *list) Filter(costLimit *uint256.Int, gasLimit uint64) (types.Transactio
return removed, invalids 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 // Cap places a hard limit on the number of items, returning all transactions
// exceeding that limit. // exceeding that limit.
func (l *list) Cap(threshold int) types.Transactions { func (l *list) Cap(threshold int) types.Transactions {

View file

@ -22,7 +22,11 @@ import (
"testing" "testing"
"github.com/holiman/uint256" "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/core/types"
"github.com/ethereum/go-ethereum/crypto" "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())
}

View file

@ -2303,10 +2303,19 @@ func (pool *TxPool) demoteUnexecutables() {
pool.enqueueTx(hash, tx, false, false) 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) { 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 there's a gap in front, alert (should never happen) and postpone all transactions
if list.Len() > 0 && list.txs.Get(nonce) == nil { if list.Len() > 0 && list.txs.Get(nonce) == nil {

View file

@ -1993,6 +1993,7 @@ func TestUnderpricing(t *testing.T) {
keys[i], _ = crypto.GenerateKey() keys[i], _ = crypto.GenerateKey()
testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000)) testAddBalance(pool, crypto.PubkeyToAddress(keys[i].PublicKey), big.NewInt(1000000))
} }
// Generate and queue a batch of transactions, both pending and queued // Generate and queue a batch of transactions, both pending and queued
txs := types.Transactions{} txs := types.Transactions{}
@ -2023,6 +2024,7 @@ func TestUnderpricing(t *testing.T) {
if err := validatePoolInternals(pool); err != nil { if err := validatePoolInternals(pool); err != nil {
t.Fatalf("pool internal state corrupted: %v", err) t.Fatalf("pool internal state corrupted: %v", err)
} }
// Ensure that adding an underpriced transaction on block limit fails // 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) { 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) 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 { if err := validatePoolInternals(pool); err != nil {
t.Fatalf("pool internal state corrupted: %v", err) t.Fatalf("pool internal state corrupted: %v", err)
} }
// Ensure that adding local transactions can push out even higher priced ones // Ensure that adding local transactions can push out even higher priced ones
ltx = pricedTransaction(1, 100000, big.NewInt(0), keys[2]) ltx = pricedTransaction(1, 100000, big.NewInt(0), keys[2])
if err := pool.AddLocal(ltx); err != nil { if err := pool.AddLocal(ltx); err != nil {
@ -2263,6 +2266,7 @@ func TestUnderpricingDynamicFee(t *testing.T) {
if err := validatePoolInternals(pool); err != nil { if err := validatePoolInternals(pool); err != nil {
t.Fatalf("pool internal state corrupted: %v", err) t.Fatalf("pool internal state corrupted: %v", err)
} }
// Ensure that adding local transactions can push out even higher priced ones // Ensure that adding local transactions can push out even higher priced ones
ltx = dynamicFeeTx(1, 100000, big.NewInt(0), big.NewInt(0), keys[2]) ltx = dynamicFeeTx(1, 100000, big.NewInt(0), big.NewInt(0), keys[2])
if err := pool.AddLocal(ltx); err != nil { if err := pool.AddLocal(ltx); err != nil {

View file

@ -188,6 +188,44 @@ func (h *Header) EmptyReceipts() bool {
return h.ReceiptHash == EmptyReceiptsHash 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 // Body is a simple (mutable, non-safe) data container for storing and moving
// a block's data contents (transactions and uncles) together. // a block's data contents (transactions and uncles) together.
type Body struct { type Body struct {

View file

@ -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)
}
}
}

View file

@ -54,6 +54,9 @@ type Transaction struct {
inner TxData // Consensus contents of a transaction inner TxData // Consensus contents of a transaction
time time.Time // Time first seen locally (spam avoidance) time time.Time // Time first seen locally (spam avoidance)
// knownAccounts (EIP-4337)
optionsAA4337 *OptionsAA4337
// caches // caches
hash atomic.Pointer[common.Hash] hash atomic.Pointer[common.Hash]
size atomic.Pointer[uint64] size atomic.Pointer[uint64]
@ -101,6 +104,16 @@ type TxData interface {
effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int 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 // EncodeRLP implements rlp.Encoder
func (tx *Transaction) EncodeRLP(w io.Writer) error { func (tx *Transaction) EncodeRLP(w io.Writer) error {
if tx.Type() == LegacyTxType { if tx.Type() == LegacyTxType {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 { 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) err := b.eth.txPool.AddLocal(signedTx)
if err != nil { if err != nil {
if unwrapped := errors.Unwrap(err); unwrapped != nil { if unwrapped := errors.Unwrap(err); unwrapped != nil {

View file

@ -361,6 +361,7 @@ func makeExtraData(extra []byte) []byte {
return extra return extra
} }
// PeerCount returns the number of connected peers.
func (s *Ethereum) PeerCount() int { func (s *Ethereum) PeerCount() int {
return s.p2pServer.PeerCount() return s.p2pServer.PeerCount()
} }

View file

@ -84,7 +84,10 @@ func (p *Peer) broadcastTransactions() {
) )
for i := 0; i < len(queue) && size < maxTxPacketSize; i++ { 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) txs = append(txs, tx)
size += common.StorageSize(tx.Size()) size += common.StorageSize(tx.Size())
} }
@ -158,7 +161,10 @@ func (p *Peer) announceTransactions() {
) )
for count = 0; count < len(queue) && size < maxTxPacketSize; count++ { 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]) pending = append(pending, queue[count])
pendingTypes = append(pendingTypes, tx.Type()) pendingTypes = append(pendingTypes, tx.Type())
pendingSizes = append(pendingSizes, uint32(tx.Size())) pendingSizes = append(pendingSizes, uint32(tx.Size()))

View file

@ -4,7 +4,9 @@ import (
"context" "context"
"github.com/ethereum/go-ethereum/common" "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/core/types"
"github.com/ethereum/go-ethereum/rpc"
) )
// GetRootHash returns root hash for given start and end block // GetRootHash returns root hash for given start and end block
@ -52,16 +54,53 @@ func (s *BlockChainAPI) appendRPCMarshalBorTransaction(ctx context.Context, bloc
return fields return fields
} }
// EthereumAPI provides an API to access Ethereum related information. // BorAPI provides an API to access Bor related information.
type BorAPI struct { type BorAPI struct {
b Backend b Backend
} }
// NewEthereumAPI creates a new Ethereum protocol API. // NewBorAPI creates a new Bor protocol API.
func NewBorAPI(b Backend) *BorAPI { func NewBorAPI(b Backend) *BorAPI {
return &BorAPI{b} 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) { 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) return api.b.GetVoteOnHash(ctx, starBlockNr, endBlockNr, hash, milestoneId)
} }

View file

@ -5383,6 +5383,13 @@ var methods = function () {
inputFormatter: [null] inputFormatter: [null]
}); });
var sendRawTransactionConditional = new Method({
name: 'sendRawTransactionConditional',
call: 'eth_sendRawTransactionConditional',
params: 2,
inputFormatter: [null]
});
var sendTransaction = new Method({ var sendTransaction = new Method({
name: 'sendTransaction', name: 'sendTransaction',
call: 'eth_sendTransaction', call: 'eth_sendTransaction',
@ -5471,6 +5478,7 @@ var methods = function () {
call, call,
estimateGas, estimateGas,
sendRawTransaction, sendRawTransaction,
sendRawTransactionConditional,
signTransaction, signTransaction,
sendTransaction, sendTransaction,
sign, sign,

View file

@ -65,6 +65,12 @@ web3._extend({
call: 'bor_getVoteOnHash', call: 'bor_getVoteOnHash',
params: 4, params: 4,
}), }),
new web3._extend.Method({
name: 'sendRawTransactionConditional',
call: 'bor_sendRawTransactionConditional',
params: 2,
inputFormatter: [null]
}),
] ]
}); });
` `

View file

@ -172,7 +172,7 @@ type mockBackend struct {
txPool *txpool.TxPool txPool *txpool.TxPool
} }
// PeerCount implements Backend. // PeerCount implements mockBackend.
func (*mockBackend) PeerCount() int { func (*mockBackend) PeerCount() int {
panic("unimplemented") panic("unimplemented")
} }

View file

@ -104,6 +104,10 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even
return miner 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. // 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 // 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 // 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 { func (miner *Miner) Mining() bool {
return miner.worker.isRunning() return miner.worker.IsRunning()
} }
func (miner *Miner) Hashrate() uint64 { func (miner *Miner) Hashrate() uint64 {

View file

@ -95,7 +95,7 @@ type testWorkerBackend struct {
uncleBlock *types.Block uncleBlock *types.Block
} }
// PeerCount implements Backend. // PeerCount implements testWorkerBackend.
func (*testWorkerBackend) PeerCount() int { func (*testWorkerBackend) PeerCount() int {
panic("unimplemented") 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, // If our sealing block contains less than 2 uncle blocks,
// add the new uncle block if valid and regenerate a new // add the new uncle block if valid and regenerate a new
// sealing block for higher profit. // 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() start := time.Now()
if err := w.commitUncle(w.current, ev.Block.Header()); err == nil { if err := w.commitUncle(w.current, ev.Block.Header()); err == nil {
commitErr := w.commit(ctx, w.current.copy(), nil, true, start) 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 // already included in the current sealing block. These transactions will
// be automatically eliminated. // be automatically eliminated.
// nolint : nestif // nolint : nestif
if !w.isRunning() && w.current != nil { if !w.IsRunning() && w.current != nil {
// If block is already full, abort // If block is already full, abort
if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas { if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas {
continue 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) { tracing.Exec(ctx, "", "worker.prepareWork", func(ctx context.Context, span trace.Span) {
// Set the coinbase if the worker is running or it's required // Set the coinbase if the worker is running or it's required
var coinbase common.Address var coinbase common.Address
if w.isRunning() { if w.IsRunning() {
if w.coinbase == (common.Address{}) { if w.coinbase == (common.Address{}) {
log.Error("Refusing to mine without etherbase") log.Error("Refusing to mine without etherbase")
return 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 // 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. // 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. // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing.

View file

@ -487,7 +487,7 @@ func (w *worker) stop() {
} }
// isRunning returns an indicator whether worker is running or not. // isRunning returns an indicator whether worker is running or not.
func (w *worker) isRunning() bool { func (w *worker) IsRunning() bool {
return w.running.Load() return w.running.Load()
} }
@ -589,7 +589,7 @@ func (w *worker) newWorkLoop(ctx context.Context, recommit time.Duration) {
case <-timer.C: case <-timer.C:
// If sealing is running resubmit a new work cycle periodically to pull in // If sealing is running resubmit a new work cycle periodically to pull in
// higher priced transactions. Disable this overhead for pending blocks. // 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. // Short circuit if no new transaction arrives.
if w.newTxs.Load() == 0 { if w.newTxs.Load() == 0 {
timer.Reset(recommit) timer.Reset(recommit)
@ -691,7 +691,7 @@ func (w *worker) mainLoop(ctx context.Context) {
} }
// If our mining block contains less than 2 uncle blocks, // If our mining block contains less than 2 uncle blocks,
// add the new uncle block if valid and regenerate a mining block. // 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() start := time.Now()
if err := w.commitUncle(w.current, ev.Block.Header()); err == nil { if err := w.commitUncle(w.current, ev.Block.Header()); err == nil {
commitErr := w.commit(ctx, w.current.copy(), nil, true, start) 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 // Note all transactions received may not be continuous with transactions
// already included in the current sealing block. These transactions will // already included in the current sealing block. These transactions will
// be automatically eliminated. // be automatically eliminated.
if !w.isRunning() && w.current != nil { // nolint : nestif
if !w.IsRunning() && w.current != nil {
// If block is already full, abort // If block is already full, abort
if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas { if gp := w.current.gasPool; gp != nil && gp.Gas() < params.TxGas {
continue continue
@ -1152,6 +1153,31 @@ mainloop:
// during transaction acceptance is the transaction pool. // during transaction acceptance is the transaction pool.
from, _ := types.Sender(env.signer, tx) 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 // Check whether the tx is replay protected. If we're not in the EIP155 hf
// phase, start ignoring the sender until we do. // phase, start ignoring the sender until we do.
if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) { if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
@ -1217,7 +1243,7 @@ mainloop:
} }
// nolint:nestif // nolint:nestif
if EnableMVHashMap && w.isRunning() { if EnableMVHashMap && w.IsRunning() {
close(chDeps) close(chDeps)
depsWg.Wait() 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 // 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. // 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. // 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) { tracing.Exec(ctx, "", "worker.prepareWork", func(ctx context.Context, span trace.Span) {
// Set the coinbase if the worker is running or it's required // Set the coinbase if the worker is running or it's required
var coinbase common.Address var coinbase common.Address
if w.isRunning() { if w.IsRunning() {
coinbase = w.etherbase() coinbase = w.etherbase()
if coinbase == (common.Address{}) { if coinbase == (common.Address{}) {
log.Error("Refusing to mine without etherbase") 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 // Note the assumption is held that the mutation is allowed to the passed env, do
// the deep copy first. // the deep copy first.
func (w *worker) commit(ctx context.Context, env *environment, interval func(), update bool, start time.Time) error { 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") ctx, span := tracing.StartSpan(ctx, "commit")
defer tracing.EndSpan(span) defer tracing.EndSpan(span)

View file

@ -133,3 +133,15 @@ type CustomError struct {
func (e *CustomError) ErrorCode() int { return e.Code } func (e *CustomError) ErrorCode() int { return e.Code }
func (e *CustomError) Error() string { return e.ValidationError } 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 }

View file

@ -375,6 +375,7 @@ func (h *handler) startCallProc(fn func(*callProc)) {
h.executionPool.Submit(context.Background(), func() error { h.executionPool.Submit(context.Background(), func() error {
defer h.callWG.Done() defer h.callWG.Done()
defer cancel() defer cancel()
fn(&callProc{ctx: ctx}) fn(&callProc{ctx: ctx})
h.executionPool.processed.Add(1) h.executionPool.processed.Add(1)

View file

@ -30,6 +30,7 @@ func (s *Server) ServeListener(l net.Listener) error {
conn, err := l.Accept() conn, err := l.Accept()
if netutil.IsTemporaryError(err) { if netutil.IsTemporaryError(err) {
log.Warn("RPC accept error", "err", err) log.Warn("RPC accept error", "err", err)
continue continue
} else if err != nil { } else if err != nil {
return err return err