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
87 changes: 82 additions & 5 deletions pkg/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,14 +536,91 @@ func (a *Adapter) GetExecutionInfo(ctx context.Context) (execution.ExecutionInfo
}

// FilterTxs implements execution.Executor.
// TODO: to implement for force inclusion
func (a *Adapter) FilterTxs(ctx context.Context, txs [][]byte, maxBytes, maxGas uint64, hasForceIncludedTransaction bool) ([]execution.FilterStatus, error) {
noFilter := make([]execution.FilterStatus, len(txs))
for i := range txs {
noFilter[i] = execution.FilterOK
// When there are no force-included txs, all txs come from the mempool
// and are already validated. No filtering needed.
if !hasForceIncludedTransaction {
result := make([]execution.FilterStatus, len(txs))
for i := range result {
result[i] = execution.FilterOK
}
return result, nil
}

result := make([]execution.FilterStatus, len(txs))

var cumulativeBytes uint64
var cumulativeGas uint64
limitReached := false

for i, tx := range txs {
// Skip empty transactions
if len(tx) == 0 {
result[i] = execution.FilterRemove
continue
}

txBytes := uint64(len(tx))
var txGas uint64

// Only validate and call CheckTx if force-included txs are present.
// Mempool txs are already validated, so we can skip CheckTx when not needed.
if hasForceIncludedTransaction {
checkTxResp, err := a.App.CheckTx(&abci.RequestCheckTx{
Tx: tx,
Type: abci.CheckTxType_New,
})
if err != nil || checkTxResp.Code != abci.CodeTypeOK {
a.Logger.Debug("filtering out invalid transaction",
"tx_index", i,
"err", err,
"code", checkTxResp.GetCode(),
"log", checkTxResp.GetLog(),
)
result[i] = execution.FilterRemove
continue
}
txGas = uint64(checkTxResp.GasWanted)

// Skip tx that can never make it in a block (too much gas)
if maxGas > 0 && txGas > maxGas {
result[i] = execution.FilterRemove
continue
}
}

// Skip tx that can never make it in a block (too big)
if maxBytes > 0 && txBytes > maxBytes {
result[i] = execution.FilterRemove
continue
}

// Once limit is reached, postpone remaining txs
if limitReached {
result[i] = execution.FilterPostpone
continue
}

// Check size limit
if maxBytes > 0 && cumulativeBytes+txBytes > maxBytes {
limitReached = true
result[i] = execution.FilterPostpone
continue
}

// Check gas limit (only when we have force-included txs and parsed the tx)
if hasForceIncludedTransaction && maxGas > 0 && cumulativeGas+txGas > maxGas {
limitReached = true
result[i] = execution.FilterPostpone
continue
}

cumulativeBytes += txBytes
cumulativeGas += txGas
result[i] = execution.FilterOK
}

return noFilter, nil
return result, nil
}

func fireEvents(
Expand Down
8 changes: 8 additions & 0 deletions pkg/adapter/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ type MockABCIApp struct {
ProcessProposalFn func(*abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error)
FinalizeBlockFn func(*abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error)
CommitFn func() (*abci.ResponseCommit, error)
CheckTxFn func(*abci.RequestCheckTx) (*abci.ResponseCheckTx, error)
}

func (m *MockABCIApp) ProcessProposal(r *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
Expand All @@ -241,3 +242,10 @@ func (m *MockABCIApp) Commit() (*abci.ResponseCommit, error) {
}
return m.CommitFn()
}

func (m *MockABCIApp) CheckTx(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
if m.CheckTxFn == nil {
panic("not expected to be called")
}
return m.CheckTxFn(req)
}
271 changes: 271 additions & 0 deletions pkg/adapter/filter_txs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package adapter

import (
"context"
"errors"
"testing"

"cosmossdk.io/log"
abci "github.com/cometbft/cometbft/abci/types"
ds "github.com/ipfs/go-datastore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/evstack/ev-node/core/execution"
)

func TestFilterTxs(t *testing.T) {
tx10bytes := make([]byte, 10)
tx20bytes := make([]byte, 20)
tx50bytes := make([]byte, 50)

okCheckTx := func(gasWanted int64) func(*abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return func(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return &abci.ResponseCheckTx{Code: abci.CodeTypeOK, GasWanted: gasWanted}, nil
}
}

// checkTxWithGasPerByte returns GasWanted proportional to tx size for deterministic gas values.
checkTxWithGasPerByte := func(gasPerByte int64) func(*abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return func(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return &abci.ResponseCheckTx{
Code: abci.CodeTypeOK,
GasWanted: int64(len(req.Tx)) * gasPerByte,
}, nil
}
}

failCheckTx := func(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return &abci.ResponseCheckTx{Code: 1, Log: "invalid tx"}, nil
}

errCheckTx := func(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
return nil, errors.New("app error")
}

specs := map[string]struct {
txs [][]byte
maxBytes uint64
maxGas uint64
hasForceIncludedTx bool
checkTxFn func(*abci.RequestCheckTx) (*abci.ResponseCheckTx, error)
expStatuses []execution.FilterStatus
expCheckTxNotCalled bool
}{
// --- No force-included txs: short-circuit, return all OK ---
"no force included - returns all OK": {
txs: [][]byte{tx10bytes, tx20bytes, tx50bytes},
maxBytes: 1, // would normally filter, but short-circuit applies
maxGas: 1,
hasForceIncludedTx: false,
expStatuses: []execution.FilterStatus{execution.FilterOK, execution.FilterOK, execution.FilterOK},
expCheckTxNotCalled: true,
},
"no force included - empty list": {
txs: [][]byte{},
maxBytes: 100,
maxGas: 100,
hasForceIncludedTx: false,
expStatuses: []execution.FilterStatus{},
expCheckTxNotCalled: true,
},

// --- Empty txs ---
"empty tx removed": {
txs: [][]byte{{}, tx10bytes},
maxBytes: 0,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(100),
expStatuses: []execution.FilterStatus{execution.FilterRemove, execution.FilterOK},
},

// --- Size filtering ---
"single tx exceeds maxBytes - removed": {
txs: [][]byte{tx50bytes},
maxBytes: 30,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(100),
expStatuses: []execution.FilterStatus{execution.FilterRemove},
},
"cumulative bytes exceeded - postpone": {
txs: [][]byte{tx20bytes, tx20bytes, tx20bytes},
maxBytes: 50,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(0),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterOK,
execution.FilterPostpone,
},
},
"remaining txs postponed after limit reached": {
txs: [][]byte{tx20bytes, tx20bytes, tx10bytes, tx10bytes},
maxBytes: 50,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(0),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterOK,
execution.FilterOK,
execution.FilterPostpone,
},
},
"maxBytes zero - no size limit": {
txs: [][]byte{tx50bytes, tx50bytes, tx50bytes},
maxBytes: 0,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(0),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterOK,
execution.FilterOK,
},
},

// --- Gas filtering ---
"single tx exceeds maxGas - removed": {
txs: [][]byte{tx10bytes},
maxBytes: 0,
maxGas: 50,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(100),
expStatuses: []execution.FilterStatus{execution.FilterRemove},
},
"cumulative gas exceeded - postpone": {
txs: [][]byte{tx10bytes, tx10bytes, tx10bytes},
maxBytes: 0,
maxGas: 250,
hasForceIncludedTx: true,
checkTxFn: checkTxWithGasPerByte(10), // 10 bytes * 10 = 100 gas each
expStatuses: []execution.FilterStatus{
execution.FilterOK, // cumulative: 100
execution.FilterOK, // cumulative: 200
execution.FilterPostpone, // 300 > 250
},
},
"maxGas zero - no gas limit": {
txs: [][]byte{tx10bytes, tx10bytes},
maxBytes: 0,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(999999),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterOK,
},
},

// --- CheckTx failure ---
"checkTx returns non-OK code - removed": {
txs: [][]byte{tx10bytes, tx10bytes},
maxBytes: 0,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: failCheckTx,
expStatuses: []execution.FilterStatus{
execution.FilterRemove,
execution.FilterRemove,
},
},
"checkTx returns error - removed": {
txs: [][]byte{tx10bytes, tx10bytes},
maxBytes: 0,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: errCheckTx,
expStatuses: []execution.FilterStatus{
execution.FilterRemove,
execution.FilterRemove,
},
},

// --- Combined bytes and gas ---
"bytes limit hit before gas limit": {
txs: [][]byte{tx50bytes, tx50bytes},
maxBytes: 60,
maxGas: 10000,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(10),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterPostpone,
},
},
"gas limit hit before bytes limit": {
txs: [][]byte{tx10bytes, tx10bytes, tx10bytes},
maxBytes: 10000,
maxGas: 15,
hasForceIncludedTx: true,
checkTxFn: checkTxWithGasPerByte(1), // 10 gas each
expStatuses: []execution.FilterStatus{
execution.FilterOK, // cumulative gas: 10
execution.FilterPostpone, // 20 > 15
execution.FilterPostpone, // limit already reached
},
},

// --- Mixed scenario ---
"mixed - invalid, ok, oversized, postponed": {
txs: [][]byte{
{}, // empty → remove
tx10bytes, // valid, fits → OK
tx10bytes, // valid, fits → OK
tx10bytes, // valid, exceeds cumulative bytes → postpone
tx10bytes, // limit reached → postpone
},
maxBytes: 25,
maxGas: 0,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(0),
expStatuses: []execution.FilterStatus{
execution.FilterRemove,
execution.FilterOK,
execution.FilterOK,
execution.FilterPostpone,
execution.FilterPostpone,
},
},
"all txs fit exactly": {
txs: [][]byte{tx10bytes, tx10bytes, tx10bytes},
maxBytes: 30,
maxGas: 300,
hasForceIncludedTx: true,
checkTxFn: okCheckTx(100),
expStatuses: []execution.FilterStatus{
execution.FilterOK,
execution.FilterOK,
execution.FilterOK,
},
},
}

for name, spec := range specs {
t.Run(name, func(t *testing.T) {
mock := &MockABCIApp{}
if !spec.expCheckTxNotCalled {
mock.CheckTxFn = spec.checkTxFn
}
// CheckTxFn intentionally left nil when expCheckTxNotCalled is true;
// if FilterTxs tries to call it, the mock will panic, catching the bug.

adapter := NewABCIExecutor(mock, ds.NewMapDatastore(), nil, nil, log.NewTestLogger(t), nil, nil)

statuses, err := adapter.FilterTxs(
context.Background(),
spec.txs,
spec.maxBytes,
spec.maxGas,
spec.hasForceIncludedTx,
)

require.NoError(t, err)
require.Len(t, statuses, len(spec.txs))
assert.Equal(t, spec.expStatuses, statuses)
})
}
}
Loading