Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction filter #5

Merged
merged 21 commits into from
Apr 8, 2024
101 changes: 101 additions & 0 deletions serve/filters/filter/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package filter

import (
"github.com/gnolang/gno/tm2/pkg/bft/types"
)

type Options struct {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
Height int64
Index uint32
notJoon marked this conversation as resolved.
Show resolved Hide resolved
GasUsed struct{ Min, Max int64 }
notJoon marked this conversation as resolved.
Show resolved Hide resolved
GasWanted struct{ Min, Max int64 }
}

// TxFilter holds a slice of transaction results.
// It provides methods to manipulate and query the transactions.
type TxFilter struct {
*baseFilter
txs []*types.TxResult
opts Options
}

// NewTxFilter creates a new TxFilter object.
func NewTxFilter(opts Options) *TxFilter {
return &TxFilter{
baseFilter: newBaseFilter(TxFilterType),
txs: make([]*types.TxResult, 0),
opts: opts,
}
}

// GetHashes iterates over all transactions in the filter and returns their hashes.
func (tf *TxFilter) GetHashes() [][]byte {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
tf.Lock()
defer tf.Unlock()

hashes := make([][]byte, 0, len(tf.txs))

for _, txr := range tf.txs {
var hash []byte

if txr != nil && txr.Tx != nil {
hash = txr.Tx.Hash()
}

hashes = append(hashes, hash)
}

return hashes
}

func (tf *TxFilter) UpdateWithTx(tx *types.TxResult) {
tf.Lock()
defer tf.Unlock()

tf.txs = append(tf.txs, tx)
}

notJoon marked this conversation as resolved.
Show resolved Hide resolved
// Apply applies all added conditions to the transactions in the filter.
//
// It returns a slice of `TxResult` that satisfy all the conditions. If no conditions are set,
// it returns all transactions in the filter.
func (tf *TxFilter) Apply() []*types.TxResult {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
tf.Lock()
defer tf.Unlock()

return checkOpts(tf.txs, tf.opts)
}

func checkOpts(txs []*types.TxResult, opts Options) []*types.TxResult {
filtered := make([]*types.TxResult, 0, len(txs))

for _, tx := range txs {
if opts.Height != 0 && tx.Height != opts.Height {
continue
}

if opts.Index != 0 && tx.Index != opts.Index {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
continue
}

if opts.GasUsed.Max != 0 && tx.Response.GasUsed > opts.GasUsed.Max {
continue
}

if opts.GasUsed.Min != 0 && tx.Response.GasUsed < opts.GasUsed.Min {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
continue
}

if opts.GasWanted.Max != 0 && tx.Response.GasWanted > opts.GasWanted.Max {
continue
}

if opts.GasWanted.Min != 0 && tx.Response.GasWanted < opts.GasWanted.Min {
notJoon marked this conversation as resolved.
Show resolved Hide resolved
continue
}

filtered = append(filtered, tx)
}

return filtered
}
290 changes: 290 additions & 0 deletions serve/filters/filter/tx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package filter

import (
"fmt"
"testing"

abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types"
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetHashes(t *testing.T) {
t.Parallel()

txs := []*types.TxResult{
{Tx: []byte(`c25dda249cdece9d908cc33adcd16aa05e20290f`)},
{Tx: []byte(`71ac9eed6a76a285ae035fe84a251d56ae9485a4`)},
{Tx: []byte(`356a192b7913b04c54574d18c28d46e6395428ab`)},
{Tx: []byte(`da4b9237bacccdf19c0760cab7aec4a8359010b0`)},
{Tx: []byte(`77de68daecd823babbb58edb1c8e14d7106e83bb`)},
{Tx: []byte(`1b6453892473a467d07372d45eb05abc2031647a`)},
{Tx: []byte(`ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4`)},
{Tx: []byte(`c1dfd96eea8cc2b62785275bca38ac261256e278`)},
}

f := NewTxFilter(Options{})

for _, tx := range txs {
f.UpdateWithTx(tx)
}

hashes := f.GetHashes()
require.Len(
t, hashes, 8,
fmt.Sprintf("There should be 8 hashes in the filter: %v", len(hashes)),
)

for i, hs := range hashes {
assert.Equal(
t, txs[i].Tx.Hash(), hs,
fmt.Sprintf("The hash should match the expected hash: %v", txs[i].Tx.Hash()),
)
}
}

func TestApplyFilters(t *testing.T) {
t.Parallel()

txs := []*types.TxResult{
{
Height: 100,
Index: 0,
Tx: []byte(`sampleTx0`),
Response: abci.ResponseDeliverTx{
GasWanted: 1000,
GasUsed: 900,
ResponseBase: abci.ResponseBase{
Data: []byte(`data0`),
},
},
},
{
Height: 101,
Index: 1,
Tx: []byte(`sampleTx1`),
Response: abci.ResponseDeliverTx{
GasWanted: 1200,
GasUsed: 1100,
ResponseBase: abci.ResponseBase{
Data: []byte(`data1`),
},
},
},
{
Height: 102,
Index: 2,
Tx: []byte(`sampleTx2`),
Response: abci.ResponseDeliverTx{
GasWanted: 1000,
GasUsed: 1400,
ResponseBase: abci.ResponseBase{
Data: []byte(`data2`),
},
},
},
{
Height: 103,
Index: 3,
Tx: []byte(`sampleTx3`),
Response: abci.ResponseDeliverTx{
GasWanted: 1200,
GasUsed: 900,
ResponseBase: abci.ResponseBase{
Data: []byte(`data3`),
},
},
},
{
Height: 104,
Index: 4,
Tx: []byte(`sampleTx4`),
Response: abci.ResponseDeliverTx{
GasWanted: 1100,
GasUsed: 1000,
ResponseBase: abci.ResponseBase{
Data: []byte(`data4`),
},
},
},
}

tests := []struct {
name string
expected []*types.TxResult
options Options
}{
{
name: "no filter",
options: Options{},
expected: txs,
},
{
name: "filter by index",
options: Options{
Index: 1,
},
expected: []*types.TxResult{txs[1]},
},
{
name: "filter by height and gas used",
options: Options{
Height: 100,
GasUsed: struct{ Min, Max int64 }{900, 1000},
},
expected: []*types.TxResult{txs[0]},
},
{
name: "filter by gas wanted 1",
options: Options{
GasWanted: struct{ Min, Max int64 }{1100, 1200},
},
expected: []*types.TxResult{txs[1], txs[3], txs[4]},
},
{
name: "filter by gas used 2",
options: Options{
GasUsed: struct{ Min, Max int64 }{900, 1000},
},
expected: []*types.TxResult{txs[0], txs[3], txs[4]},
},
{
name: "filter by gas wanted is invalid",
options: Options{
GasWanted: struct{ Min, Max int64 }{1200, 1100},
},
expected: []*types.TxResult{},
},
{
name: "gas used filter is invalid",
options: Options{
GasUsed: struct{ Min, Max int64 }{1000, 900},
},
expected: []*types.TxResult{},
},
{
name: "use all filters",
options: Options{
Height: 100,
Index: 0,
GasUsed: struct{ Min, Max int64 }{900, 1000},
GasWanted: struct{ Min, Max int64 }{1000, 1100},
},
expected: []*types.TxResult{txs[0]},
},
{
name: "use all filters but sequence is flipped",
options: Options{
GasWanted: struct{ Min, Max int64 }{1000, 1100},
GasUsed: struct{ Min, Max int64 }{900, 1000},
Index: 0,
Height: 100,
},
expected: []*types.TxResult{txs[0]},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

f := NewTxFilter(tt.options)

for _, tx := range txs {
f.UpdateWithTx(tx)
}

filtered := f.Apply()
require.Len(
t, filtered, len(tt.expected),
fmt.Sprintf(
"There should be one transaction after applying filters: %v",
len(tt.expected),
),
)

for i, tx := range filtered {
assert.Equal(
t, tt.expected[i], tx,
fmt.Sprintf(
"The filtered transaction should match the expected transaction: %v",
tt.expected[i],
),
)
}
})
}
}

func TestApplyFiltersWithLargeData(t *testing.T) {
t.Parallel()

const txCount = 100000

txs := make([]*types.TxResult, txCount)

for i := 0; i < txCount; i++ {
txs[i] = &types.TxResult{
Height: int64(i / 10000),
Index: uint32(i),
Tx: []byte(fmt.Sprintf("sampleTx%d", i)),
Response: abci.ResponseDeliverTx{
GasWanted: int64(1000 + i%200),
GasUsed: int64(900 + i%100),
ResponseBase: abci.ResponseBase{
Data: []byte(fmt.Sprintf("data%d", i)),
},
},
}
}

tests := []struct {
name string
options Options
expected int
}{
{
name: "no filter",
options: Options{},
expected: txCount,
},
{
name: "filter by height",
options: Options{
Height: 5,
},
expected: txCount / 10,
},
{
name: "filter by gas used",
options: Options{
GasUsed: struct{ Min, Max int64 }{950, 1000},
},
expected: txCount / 2,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

f := NewTxFilter(tt.options)

for _, tx := range txs {
f.UpdateWithTx(tx)
}

filtered := f.Apply()
require.Len(
t, filtered, tt.expected,
fmt.Sprintf(
"There should be %d transactions after applying filters. got %d",
tt.expected, len(filtered),
),
)
})
}
}
1 change: 1 addition & 0 deletions serve/filters/filter/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ type Type string

const (
BlockFilterType Type = "BlockFilter"
TxFilterType Type = "TxFilter"
)
Loading