Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2189,6 +2189,13 @@ func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil
}
}

// On a node that relays private transactions, route every raw tx through the
// private path so it never enters the public mempool. Private takes precedence
// over preconf; clients wanting preconf must use SendRawTransactionForPreconf.
if api.b.PrivateTxEnabled() {
return api.submitPrivateTransaction(ctx, tx)
}

hash, err := SubmitTransaction(ctx, api.b, tx)

// If preconf is enabled, submit tx directly to BP
Expand Down Expand Up @@ -2226,7 +2233,17 @@ func (api *TransactionAPI) SendRawTransactionSync(ctx context.Context, input hex
sub := api.b.SubscribeChainEvent(ch)
defer sub.Unsubscribe()

hash, err := SubmitTransaction(ctx, api.b, tx)
var (
hash common.Hash
err error
)
// On a private-tx-enabled node, route through the private path so the tx is
// not gossiped publicly. Receipt-wait below works the same either way.
if api.b.PrivateTxEnabled() {
hash, err = api.submitPrivateTransaction(ctx, tx)
} else {
hash, err = SubmitTransaction(ctx, api.b, tx)
}
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2339,7 +2356,7 @@ func (api *TransactionAPI) SendRawTransactionForPreconf(ctx context.Context, inp
}, nil
}

// SendRawTransactionForPreconf will accept a private transaction from relay if enabled. It will ensure
// SendRawTransactionPrivate will accept a private transaction from relay if enabled. It will ensure
// that the transaction is not gossiped over public network.
func (api *TransactionAPI) SendRawTransactionPrivate(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
if !api.b.AcceptPrivateTxs() && !api.b.PrivateTxEnabled() {
Expand All @@ -2351,7 +2368,13 @@ func (api *TransactionAPI) SendRawTransactionPrivate(ctx context.Context, input
return common.Hash{}, err
}

// Track the tx hash to ensure it is not gossiped in public
return api.submitPrivateTransaction(ctx, tx)
}

// submitPrivateTransaction tracks the tx hash so it is not gossiped on the
// public network, submits it to the local pool, and (when this node is
// configured to relay) forwards it directly to block producers.
func (api *TransactionAPI) submitPrivateTransaction(ctx context.Context, tx *types.Transaction) (common.Hash, error) {
api.b.RecordPrivateTx(tx.Hash())

hash, err := SubmitTransaction(ctx, api.b, tx)
Expand Down
147 changes: 147 additions & 0 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5003,6 +5003,109 @@ func TestSendRawTransaction_PreconfPath(t *testing.T) {
require.Equal(t, tx.Hash(), hash)
})
}

func TestSendRawTransaction_PrivatePath(t *testing.T) {
t.Parallel()

t.Run("private enabled, routes through private path", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true

var recordCount, submitPrivateCount atomic.Int32
b.recordPrivateTxFn = func(hash common.Hash) { recordCount.Add(1) }
b.submitPrivateTxFn = func(tx *types.Transaction) error { submitPrivateCount.Add(1); return nil }

api := NewTransactionAPI(b, new(AddrLocker))
raw, tx := makeSelfSignedRaw(t, api, b.acc.Address)

hash, err := api.SendRawTransaction(context.Background(), raw)
require.NoError(t, err)
require.Equal(t, tx.Hash(), hash)
require.Equal(t, int32(1), recordCount.Load(), "RecordPrivateTx should be called once")
require.Equal(t, int32(1), submitPrivateCount.Load(), "SubmitPrivateTx should be called once")
})

t.Run("private precedence: preconf NOT called when both enabled", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.preconfEnabled = true

var preconfCount, submitPrivateCount atomic.Int32
b.submitTxForPreconfFn = func(tx *types.Transaction) error { preconfCount.Add(1); return nil }
b.submitPrivateTxFn = func(tx *types.Transaction) error { submitPrivateCount.Add(1); return nil }

api := NewTransactionAPI(b, new(AddrLocker))
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)

_, err := api.SendRawTransaction(context.Background(), raw)
require.NoError(t, err)
require.Equal(t, int32(1), submitPrivateCount.Load(), "SubmitPrivateTx should be called when private is enabled")
require.Equal(t, int32(0), preconfCount.Load(), "SubmitTxForPreconf should NOT be called when private path is taken")
})

t.Run("private enabled, SubmitPrivateTx fails returns wrapped error", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.submitPrivateTxFn = func(tx *types.Transaction) error { return errors.New("relay down") }

api := NewTransactionAPI(b, new(AddrLocker))
raw, tx := makeSelfSignedRaw(t, api, b.acc.Address)

hash, err := api.SendRawTransaction(context.Background(), raw)
require.Error(t, err)
require.Contains(t, err.Error(), "private tx accepted locally, submission failed")
require.Contains(t, err.Error(), "relay down")
require.Equal(t, tx.Hash(), hash, "hash should be returned even on SubmitPrivateTx failure")
})

t.Run("private enabled, SendTx fails purges and returns error", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.sendTxErr = errors.New("pool full")

var purgeCount, submitPrivateCount atomic.Int32
b.purgePrivateTxFn = func(hash common.Hash) { purgeCount.Add(1) }
b.submitPrivateTxFn = func(tx *types.Transaction) error { submitPrivateCount.Add(1); return nil }

api := NewTransactionAPI(b, new(AddrLocker))
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)

_, err := api.SendRawTransaction(context.Background(), raw)
require.Error(t, err)
require.Contains(t, err.Error(), "pool full")
require.Equal(t, int32(1), purgeCount.Load(), "PurgePrivateTx should be called on SendTx failure")
require.Equal(t, int32(0), submitPrivateCount.Load(), "SubmitPrivateTx should NOT be called when SendTx fails")
})

t.Run("private enabled, ErrAlreadyKnown does not purge but still submits to BPs", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.sendTxErr = txpool.ErrAlreadyKnown

var purgeCount, submitPrivateCount atomic.Int32
b.purgePrivateTxFn = func(hash common.Hash) { purgeCount.Add(1) }
b.submitPrivateTxFn = func(tx *types.Transaction) error { submitPrivateCount.Add(1); return nil }

api := NewTransactionAPI(b, new(AddrLocker))
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)

_, err := api.SendRawTransaction(context.Background(), raw)
require.ErrorIs(t, err, txpool.ErrAlreadyKnown)
require.Equal(t, int32(0), purgeCount.Load(), "PurgePrivateTx should NOT be called for ErrAlreadyKnown")
require.Equal(t, int32(1), submitPrivateCount.Load(), "SubmitPrivateTx should still be called for ErrAlreadyKnown")
})
}

func (b *testBackend) ProtocolVersion() uint {
return 69 // ETH69
}
Expand Down Expand Up @@ -5141,6 +5244,50 @@ func TestSendRawTransactionSync_Timeout(t *testing.T) {
}
}

func TestSendRawTransactionSync_PrivatePath(t *testing.T) {
t.Parallel()

t.Run("private enabled, routes through private path and returns receipt", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.autoMine = true

var recordCount, submitPrivateCount atomic.Int32
b.recordPrivateTxFn = func(hash common.Hash) { recordCount.Add(1) }
b.submitPrivateTxFn = func(tx *types.Transaction) error { submitPrivateCount.Add(1); return nil }

api := NewTransactionAPI(b, new(AddrLocker))
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)

receipt, err := api.SendRawTransactionSync(context.Background(), raw, nil)
require.NoError(t, err)
require.NotNil(t, receipt)
require.Contains(t, receipt, "blockNumber")
require.Equal(t, int32(1), recordCount.Load(), "RecordPrivateTx should be called once")
require.Equal(t, int32(1), submitPrivateCount.Load(), "SubmitPrivateTx should be called once")
})

t.Run("private enabled, SubmitPrivateTx fails returns wrapped error and no receipt", func(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{Config: params.TestChainConfig, Alloc: types.GenesisAlloc{}}
b := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
b.privateTxEnabled = true
b.autoMine = true
b.submitPrivateTxFn = func(tx *types.Transaction) error { return errors.New("relay down") }

api := NewTransactionAPI(b, new(AddrLocker))
raw, _ := makeSelfSignedRaw(t, api, b.acc.Address)

receipt, err := api.SendRawTransactionSync(context.Background(), raw, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "private tx accepted locally, submission failed")
require.Contains(t, err.Error(), "relay down")
require.Nil(t, receipt)
})
}

func TestCoinbase(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion params/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
const (
VersionMajor = 2 // Major version component of the current release
VersionMinor = 7 // Minor version component of the current release
VersionPatch = 2 // Patch version component of the current release
VersionPatch = 3 // Patch version component of the current release
VersionMeta = "" // Version metadata to append to the version string
)

Expand Down
Loading