Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
70 changes: 61 additions & 9 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ import (
// allowed to produce in order to speed up calculations.
const estimateGasErrorRatio = 0.015

var errBlobTxNotSupported = errors.New("signing blob transactions not supported")
var errSubClosed = errors.New("chain subscription closed")
var (
errBlobTxNotSupported = errors.New("signing blob transactions not supported")
errSubClosed = errors.New("chain subscription closed")
errPreconfNotEnabled = errors.New("preconf transactions are not accepted on this node")
errPrivateTxNotEnabled = errors.New("private transactions are not accepted on this node")
)

// EthereumAPI provides an API to access Ethereum related information.
type EthereumAPI struct {
Expand Down Expand Up @@ -2189,6 +2193,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 +2237,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 @@ -2300,10 +2321,11 @@ func (api *TransactionAPI) SendRawTransactionSync(ctx context.Context, input hex
}

// SendRawTransactionForPreconf will accept a preconf transaction from relay if enabled. It will
// offer a soft inclusion confirmation if the transaction is accepted into the pending pool.
// offer a soft inclusion confirmation if the transaction is accepted into the pending pool. Note
// that this is an internal API used only by the relay for submitting transactions.
func (api *TransactionAPI) SendRawTransactionForPreconf(ctx context.Context, input hexutil.Bytes) (map[string]interface{}, error) {
if !api.b.AcceptPreconfTxs() {
return nil, errors.New("preconf transactions are not accepted on this node")
return nil, errPreconfNotEnabled
}

tx := new(types.Transaction)
Expand Down Expand Up @@ -2339,19 +2361,49 @@ func (api *TransactionAPI) SendRawTransactionForPreconf(ctx context.Context, inp
}, nil
}

// SendRawTransactionForPreconf will accept a private transaction from relay if enabled. It will ensure
// SendRawTransactionPreconf will accept a preconf transaction if allowed. It will offer a
// soft inclusion confirmation if the transaction is accepted into the pending pool. Note
// that this is an external API.
func (api *TransactionAPI) SendRawTransactionPreconf(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
if !api.b.PreconfEnabled() {
return common.Hash{}, errPreconfNotEnabled
}

tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
return common.Hash{}, err
}

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

// Submit tx directly to BP. Preconf processing mostly happens in background so don't
// float the error back to the user.
if err := api.b.SubmitTxForPreconf(tx); err != nil {
log.Error("Transaction accepted locally but submission for preconf failed", "err", err)
}

return hash, err
}

// 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() {
return common.Hash{}, errors.New("private transactions are not accepted on this node")
return common.Hash{}, errPrivateTxNotEnabled
}

tx := new(types.Transaction)
if err := tx.UnmarshalBinary(input); err != nil {
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 All @@ -2377,7 +2429,7 @@ func (api *TransactionAPI) SendRawTransactionPrivate(ctx context.Context, input

func (api *TransactionAPI) CheckPreconfStatus(ctx context.Context, hash common.Hash) (bool, error) {
if !api.b.PreconfEnabled() {
return false, errors.New("preconf transactions are not accepted on this node")
return false, errPreconfNotEnabled
}
return api.b.CheckPreconfStatus(hash)
}
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