Skip to content
Merged
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
64 changes: 33 additions & 31 deletions core/txpool/ingress_filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"context"
"time"

"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/log"
)

// IngressFilter is an interface that allows filtering of transactions before they are added to the transaction pool.
Expand All @@ -16,44 +16,46 @@ type IngressFilter interface {
FilterTx(ctx context.Context, tx *types.Transaction) bool
}

type interopFilter struct {
logsFn func(tx *types.Transaction) (logs []*types.Log, logTimestamp uint64, err error)
checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error
type interopFilterAPI interface {
CurrentInteropBlockTime() (uint64, error)
TxToInteropAccessList(tx *types.Transaction) []common.Hash
CheckAccessList(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, execDesc interoptypes.ExecutingDescriptor) error
}

func NewInteropFilter(
logsFn func(tx *types.Transaction) ([]*types.Log, uint64, error),
checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error) IngressFilter {
return &interopFilter{
logsFn: logsFn,
checkFn: checkFn,
type interopAccessFilter struct {
api interopFilterAPI
timeout uint64
}

// NewInteropFilter creates a new IngressFilter that filters transactions based on the interop access list.
// the timeout is set to 1 day, the specified preverifier window
func NewInteropFilter(api interopFilterAPI) IngressFilter {
return &interopAccessFilter{
api: api,
timeout: 86400,
}
}

// 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, logTimestamp, 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
// it uses provided functions to get the access list from the transaction
// and check it against the supervisor
func (f *interopAccessFilter) FilterTx(ctx context.Context, tx *types.Transaction) bool {
hashes := f.api.TxToInteropAccessList(tx)
// if there are no interop access list entries, allow the transaction (there is no interop check to perform)
if len(hashes) == 0 {
return true
}
if len(logs) == 0 {
return true // default to allow if there are no logs
}
ems, err := interoptypes.ExecutingMessagesFromLogs(logs)
t, err := f.api.CurrentInteropBlockTime()
// if there are interop access list entries, but the interop API is not available, reject the transaction
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
return false
}
if len(ems) == 0 {
return true // default to allow if there are no executing messages
// if the transaction is older than the preverifier window, reject it eagerly
expireTime := time.Unix(int64(t), 0).Add(time.Duration(-f.timeout) * time.Second)
if tx.Time().Compare(expireTime) < 0 {
return false
}

ctx, cancel := context.WithTimeout(ctx, time.Second*2)
defer cancel()
// check with the supervisor if the transaction should be allowed given the executing messages
// the message can be unsafe (discovered only via P2P unsafe blocks), but it must be cross-valid
// so CrossUnsafe is used here
return f.checkFn(ctx, ems, interoptypes.CrossUnsafe, logTimestamp) == nil
exDesc := interoptypes.ExecutingDescriptor{Timestamp: t, Timeout: f.timeout}
// perform the interop check
return f.api.CheckAccessList(ctx, hashes, interoptypes.CrossUnsafe, exDesc) == nil
}
189 changes: 81 additions & 108 deletions core/txpool/ingress_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,126 +3,113 @@ package txpool
import (
"context"
"errors"
"math/big"
"net"
"testing"
"time"

"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"
)

type mockInteropFilterAPI struct {
timeFn func() (uint64, error)
accessListFn func(tx *types.Transaction) []common.Hash
checkFn func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error
}

func (m *mockInteropFilterAPI) CurrentInteropBlockTime() (uint64, error) {
if m.timeFn != nil {
return m.timeFn()
}
return 0, nil
}

func (m *mockInteropFilterAPI) TxToInteropAccessList(tx *types.Transaction) []common.Hash {
if m.accessListFn != nil {
return m.accessListFn(tx)
}
return nil
}

func (m *mockInteropFilterAPI) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error {
if m.checkFn != nil {
return m.checkFn(ctx, inboxEntries, minSafety, ed)
}
return nil
}

func TestInteropFilter(t *testing.T) {
// some placeholder transaction to test with
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: big.NewInt(1),
Nonce: 1,
To: &common.Address{},
Value: big.NewInt(1),
Data: []byte{},
})
t.Run("Tx has no logs", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, uint64, error) {
return []*types.Log{}, 0, nil
api := &mockInteropFilterAPI{}
filter := NewInteropFilter(api)
tx := types.NewTx(&types.DynamicFeeTx{})

t.Run("Tx has no access list", func(t *testing.T) {
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return nil
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
// make this return error, but it won't be called because logs are empty
return errors.New("error")
require.True(t, filter.FilterTx(context.Background(), tx))
})
t.Run("Tx errored when checking current interop block time", func(t *testing.T) {
api.timeFn = func() (uint64, error) {
return 0, 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(), tx))
})
t.Run("Tx errored when getting logs", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, uint64, error) {
return []*types.Log{}, 0, errors.New("error")
t.Run("Tx has valid executing message", func(t *testing.T) {
api.timeFn = func() (uint64, error) {
return 0, nil
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
// make this return error, but it won't be called because logs retrieval errored
return errors.New("error")
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return []common.Hash{{0xaa}}
}
// when log retrieval errors, the transaction should be denied
filter := NewInteropFilter(logFn, checkFn)
require.False(t, filter.FilterTx(context.Background(), tx))
api.checkFn = func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error {
require.Equal(t, common.Hash{0xaa}, inboxEntries[0])
return nil
}
require.True(t, filter.FilterTx(context.Background(), tx))
})
t.Run("Tx has no executing messages", func(t *testing.T) {
logFn := func(tx *types.Transaction) ([]*types.Log, uint64, error) {
l1 := &types.Log{
Topics: []common.Hash{common.BytesToHash([]byte("topic1"))},
}
return []*types.Log{l1}, 0, nil
t.Run("Tx has invalid executing message", func(t *testing.T) {
api.timeFn = func() (uint64, error) {
return 1, nil
}
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
// make this return error, but it won't be called because logs retrieval doesn't have executing messages
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return []common.Hash{{0xaa}}
}
api.checkFn = func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error {
require.Equal(t, common.Hash{0xaa}, inboxEntries[0])
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(), tx))
require.False(t, filter.FilterTx(context.Background(), tx))
})
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)
t.Run("Tx has valid executing message equal to than expiry", func(t *testing.T) {
api.timeFn = func() (uint64, error) {
expiredT := tx.Time().Add(86400 * time.Second)
return uint64(expiredT.Unix()), nil
}
logFn := func(tx *types.Transaction) ([]*types.Log, uint64, error) {
return []*types.Log{l1}, 0, nil
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return []common.Hash{{0xaa}}
}
var spyEMs []interoptypes.Message
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
spyEMs = ems
api.checkFn = func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error {
require.Equal(t, common.Hash{0xaa}, inboxEntries[0])
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(), tx))
// 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)
t.Run("Tx has valid executing message older than expiry", func(t *testing.T) {
api.timeFn = func() (uint64, error) {
expiredT := tx.Time().Add(86401 * time.Second)
return uint64(expiredT.Unix()), nil
}
logFn := func(tx *types.Transaction) ([]*types.Log, uint64, error) {
return []*types.Log{l1}, 0, nil
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return []common.Hash{{0xaa}}
}
var spyEMs []interoptypes.Message
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
spyEMs = ems
return errors.New("error")
api.checkFn = func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) error {
require.Equal(t, common.Hash{0xaa}, inboxEntries[0])
return nil
}
// 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(), tx))
// confirm that one executing message was passed to the checkFn
require.Equal(t, 1, len(spyEMs))
})
}

Expand All @@ -149,38 +136,24 @@ func TestInteropFilterRPCFailures(t *testing.T) {

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, uint64, 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}, 0, nil
api := &mockInteropFilterAPI{}
filter := NewInteropFilter(api)
api.accessListFn = func(tx *types.Transaction) []common.Hash {
return []common.Hash{{0xaa}}
}

// Create mock check function that simulates RPC failures
checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel, emsTimestamp uint64) error {
api.checkFn = func(ctx context.Context, inboxEntries []common.Hash, minSafety interoptypes.SafetyLevel, ed interoptypes.ExecutingDescriptor) 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")
})
Expand Down
21 changes: 21 additions & 0 deletions core/types/interoptypes/interop.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,18 @@ const (

type ExecutingDescriptor struct {
Timestamp uint64
Timeout uint64
}

type executingDescriptorMarshaling struct {
Timestamp hexutil.Uint64 `json:"timestamp"`
Timeout hexutil.Uint64 `json:"timeout"`
}

func (ed ExecutingDescriptor) MarshalJSON() ([]byte, error) {
var enc executingDescriptorMarshaling
enc.Timestamp = hexutil.Uint64(ed.Timestamp)
enc.Timeout = hexutil.Uint64(ed.Timeout)
return json.Marshal(&enc)
}

Expand All @@ -188,5 +191,23 @@ func (ed *ExecutingDescriptor) UnmarshalJSON(input []byte) error {
return err
}
ed.Timestamp = uint64(dec.Timestamp)
ed.Timeout = uint64(dec.Timeout)
return nil
}

func TxToInteropAccessList(tx *types.Transaction) []common.Hash {
if tx == nil {
return nil
}
al := tx.AccessList()
if len(al) == 0 {
return nil
}
var hashes []common.Hash
for i := range al {
if al[i].Address == params.InteropCrossL2InboxAddress {
hashes = append(hashes, al[i].StorageKeys...)
}
}
return hashes
}
Loading