-
Notifications
You must be signed in to change notification settings - Fork 794
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
interop: Ingress Filtering for Interop Enabled Mempool
Co-authored-by: Tyler Smith <mail@tcry.pt> Co-authored-by: Diederik Loerakker <proto@protolambda.com>
- Loading branch information
1 parent
48cf9ac
commit ef9a639
Showing
10 changed files
with
472 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.