Skip to content

Commit

Permalink
interop: Ingress Filtering for Interop Enabled Mempool
Browse files Browse the repository at this point in the history
Co-authored-by: Tyler Smith <mail@tcry.pt>
Co-authored-by: Diederik Loerakker <proto@protolambda.com>
  • Loading branch information
3 people committed Nov 1, 2024
1 parent 48cf9ac commit 007538b
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 19 deletions.
4 changes: 4 additions & 0 deletions core/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ var (
// current network configuration.
ErrTxTypeNotSupported = types.ErrTxTypeNotSupported

// ErrTxFilteredOut indicates an ingress filter has rejected the transaction from
// being included in the pool.
ErrTxFilteredOut = errors.New("transaction filtered out")

// ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a
// transaction with a tip higher than the total fee cap.
ErrTipAboveFeeCap = errors.New("max priority fee per gas higher than max fee per gas")
Expand Down
57 changes: 57 additions & 0 deletions core/txpool/ingress_filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package txpool

import (
"context"
"time"

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/interoptypes"
"github.com/ethereum/go-ethereum/log"
)

// IngressFilter is an interface that allows filtering of transactions before they are added to the transaction pool.
// Implementations of this interface can be used to filter transactions based on various criteria.
// FilterTx will return true if the transaction should be allowed, and false if it should be rejected.
type IngressFilter interface {
FilterTx(ctx context.Context, tx *types.Transaction) bool
}

type interopFilter struct {
logsFn func(tx *types.Transaction) ([]*types.Log, error)
checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error
}

func NewInteropFilter(
logsFn func(tx *types.Transaction) ([]*types.Log, error),
checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error) IngressFilter {
return &interopFilter{
logsFn: logsFn,
checkFn: checkFn,
}
}

// FilterTx implements IngressFilter.FilterTx
// it gets logs checks for message safety based on the function provided
func (f *interopFilter) FilterTx(ctx context.Context, tx *types.Transaction) bool {
logs, err := f.logsFn(tx)
if err != nil {
log.Debug("Failed to retrieve logs of tx", "txHash", tx.Hash(), "err", err)
return false // default to deny if logs cannot be retrieved
}
if len(logs) == 0 {
return true // default to allow if there are no logs
}
ems, err := interoptypes.ExecutingMessagesFromLogs(logs)
if err != nil {
log.Debug("Failed to parse executing messages of tx", "txHash", tx.Hash(), "err", err)
return false // default to deny if logs cannot be parsed
}
if len(ems) == 0 {
return true // default to allow if there are no executing messages
}

ctx, cancel := context.WithTimeout(ctx, time.Second*2)
defer cancel()
// check with the supervisor if the transaction should be allowed given the executing messages
return f.checkFn(ctx, ems, interoptypes.Unsafe) == nil
}
302 changes: 302 additions & 0 deletions core/txpool/ingress_filters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
package txpool

import (
"context"
"errors"
"net"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/interoptypes"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)

func TestInteropFilter(t *testing.T) {
t.Run("Tx has no logs", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
return []*types.Log{}, nil
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
// make this return error, but it won't be called because logs are empty
return errors.New("error")
}
// when there are no logs to process, the transaction should be allowed
filter := NewInteropFilter(logFn, checkFn)
require.True(t, filter.FilterTx(context.Background(), &types.Transaction{}))
})
t.Run("Tx errored when getting logs", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
return []*types.Log{}, errors.New("error")
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
// make this return error, but it won't be called because logs retrieval errored
return errors.New("error")
}
// when log retrieval errors, the transaction should be allowed
filter := NewInteropFilter(logFn, checkFn)
require.True(t, filter.FilterTx(context.Background(), &types.Transaction{}))
})
t.Run("Tx has no executing messages", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
l1 := &types.Log{
Topics: []common.Hash{common.BytesToHash([]byte("topic1"))},
}
return []*types.Log{l1}, errors.New("error")
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
// make this return error, but it won't be called because logs retrieval doesn't have executing messages
return errors.New("error")
}
// when no executing messages are included, the transaction should be allowed
filter := NewInteropFilter(logFn, checkFn)
require.True(t, filter.FilterTx(context.Background(), &types.Transaction{}))
})
t.Run("Tx has valid executing message", func(t *testing.T) {
// build a basic executing message
// the executing message must pass basic decode validation,
// but the validity check is done by the checkFn
l1 := &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: []byte{},
}
// using all 0s for data allows all takeZeros to pass
for i := 0; i < 32*5; i++ {
l1.Data = append(l1.Data, 0)
}
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
return []*types.Log{l1}, nil
}
var spyEMs []interoptypes.Message
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
spyEMs = ems
return nil
}
// when there is one executing message, the transaction should be allowed
// if the checkFn returns nil
filter := NewInteropFilter(logFn, checkFn)
require.True(t, filter.FilterTx(context.Background(), &types.Transaction{}))
// confirm that one executing message was passed to the checkFn
require.Equal(t, 1, len(spyEMs))
})
t.Run("Tx has invalid executing message", func(t *testing.T) {
// build a basic executing message
// the executing message must pass basic decode validation,
// but the validity check is done by the checkFn
l1 := &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: []byte{},
}
// using all 0s for data allows all takeZeros to pass
for i := 0; i < 32*5; i++ {
l1.Data = append(l1.Data, 0)
}
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
return []*types.Log{l1}, nil
}
var spyEMs []interoptypes.Message
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
spyEMs = ems
return errors.New("error")
}
// when there is one executing message, and the checkFn returns an error,
// (ie the supervisor rejects the transaction) the transaction should be denied
filter := NewInteropFilter(logFn, checkFn)
require.False(t, filter.FilterTx(context.Background(), &types.Transaction{}))
// confirm that one executing message was passed to the checkFn
require.Equal(t, 1, len(spyEMs))
})
}

func TestInteropFilterRPCFailures(t *testing.T) {
tests := []struct {
name string
networkErr bool
timeout bool
invalidResp bool
}{
{
name: "Network Error",
networkErr: true,
},
{
name: "Timeout",
timeout: true,
},
{
name: "Invalid Response",
invalidResp: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock log function that always returns our test log
logFn := func(tx *types.Transaction) ([]*types.Log, error) {
log := &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: make([]byte, 32*5),
}
return []*types.Log{log}, nil
}

// Create mock check function that simulates RPC failures
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error {
if tt.networkErr {
return &net.OpError{Op: "dial", Err: errors.New("connection refused")}
}

if tt.timeout {
return context.DeadlineExceeded
}

if tt.invalidResp {
return errors.New("invalid response format")
}

return nil
}

// Create and test filter
filter := NewInteropFilter(logFn, checkFn)
result := filter.FilterTx(context.Background(), &types.Transaction{})
require.Equal(t, false, result, "FilterTx result mismatch")
})
}
}

func TestInteropMessageFormatEdgeCases(t *testing.T) {
tests := []struct {
name string
log *types.Log
expectedError string
}{
{
name: "Empty Topics",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{},
Data: make([]byte, 32*5),
},
expectedError: "unexpected number of event topics: 0",
},
{
name: "Wrong Event Topic",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash([]byte("wrong topic")),
common.BytesToHash([]byte("payloadHash")),
},
Data: make([]byte, 32*5),
},
expectedError: "unexpected event topic",
},
{
name: "Missing PayloadHash Topic",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
},
Data: make([]byte, 32*5),
},
expectedError: "unexpected number of event topics: 1",
},
{
name: "Too Many Topics",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
common.BytesToHash([]byte("extra")),
},
Data: make([]byte, 32*5),
},
expectedError: "unexpected number of event topics: 3",
},
{
name: "Data Too Short",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: make([]byte, 32*4), // One word too short
},
expectedError: "unexpected identifier data length: 128",
},
{
name: "Data Too Long",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: make([]byte, 32*6), // One word too long
},
expectedError: "unexpected identifier data length: 192",
},
{
name: "Invalid Address Padding",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: func() []byte {
data := make([]byte, 32*5)
data[0] = 1 // Add non-zero byte in address padding
return data
}(),
},
expectedError: "invalid address padding",
},
{
name: "Invalid Block Number Padding",
log: &types.Log{
Address: params.InteropCrossL2InboxAddress,
Topics: []common.Hash{
common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]),
common.BytesToHash([]byte("payloadHash")),
},
Data: func() []byte {
data := make([]byte, 32*5)
data[32+23] = 1 // Add non-zero byte in block number padding
return data
}(),
},
expectedError: "invalid block number padding",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var msg interoptypes.Message
err := msg.DecodeEvent(tt.log.Topics, tt.log.Data)
if tt.expectedError != "" {
require.Error(t, err)
require.ErrorContains(t, err, tt.expectedError)
} else {
require.NoError(t, err)
}
})
}
}
Loading

0 comments on commit 007538b

Please sign in to comment.