Skip to content

Commit 6a5c937

Browse files
authored
Implement standalone transaction prioritizer (#2320)
Extract the logic of transaction prioritisation from various Ante handlers for both EVM and cosmos transactions into a side effect free lightweight API exposed via ABCI interface. Relates to: * sei-protocol/sei-tendermint#301 * ~sei-protocol/sei-cosmos#598 Repo archived, and changes are ported to this PR.
1 parent 5820ece commit 6a5c937

16 files changed

Lines changed: 466 additions & 15 deletions

File tree

app/app.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ type App struct {
394394
wsServerStartSignal chan struct{}
395395
httpServerStartSignalSent bool
396396
wsServerStartSignalSent bool
397+
398+
txPrioritizer sdk.TxPrioritizer
397399
}
398400

399401
type AppOption func(*App)
@@ -985,6 +987,8 @@ func New(
985987

986988
app.RegisterDeliverTxHook(app.AddCosmosEventsToEVMReceiptIfApplicable)
987989

990+
app.txPrioritizer = NewSeiTxPrioritizer(logger, &app.EvmKeeper, &app.UpgradeKeeper, &app.ParamsKeeper).GetTxPriorityHint
991+
app.SetTxPrioritizer(app.txPrioritizer)
988992
return app
989993
}
990994

app/prioritizer.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package app
2+
3+
import (
4+
"math"
5+
"math/big"
6+
7+
sdk "github.com/cosmos/cosmos-sdk/types"
8+
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
9+
cosmosante "github.com/cosmos/cosmos-sdk/x/auth/ante"
10+
paramskeeper "github.com/cosmos/cosmos-sdk/x/params/keeper"
11+
upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper"
12+
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
13+
ethtypes "github.com/ethereum/go-ethereum/core/types"
14+
"github.com/sei-protocol/sei-chain/app/antedecorators"
15+
"github.com/sei-protocol/sei-chain/utils"
16+
evmante "github.com/sei-protocol/sei-chain/x/evm/ante"
17+
"github.com/sei-protocol/sei-chain/x/evm/derived"
18+
evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper"
19+
evmtypes "github.com/sei-protocol/sei-chain/x/evm/types"
20+
oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types"
21+
"github.com/tendermint/tendermint/libs/log"
22+
)
23+
24+
var _ sdk.TxPrioritizer = (*SeiTxPrioritizer)(nil).GetTxPriorityHint
25+
26+
type SeiTxPrioritizer struct {
27+
evmKeeper *evmkeeper.Keeper
28+
upgradeKeeper *upgradekeeper.Keeper
29+
paramsKeeper *paramskeeper.Keeper
30+
logger log.Logger
31+
}
32+
33+
func NewSeiTxPrioritizer(logger log.Logger, ek *evmkeeper.Keeper, uk *upgradekeeper.Keeper, pk *paramskeeper.Keeper) *SeiTxPrioritizer {
34+
return &SeiTxPrioritizer{
35+
logger: logger,
36+
evmKeeper: ek,
37+
upgradeKeeper: uk,
38+
paramsKeeper: pk,
39+
}
40+
}
41+
42+
func (s *SeiTxPrioritizer) GetTxPriorityHint(ctx sdk.Context, tx sdk.Tx) (_priorityHint int64, _err error) {
43+
defer func() {
44+
if r := recover(); r != nil {
45+
// Fall back to no-op priority if we panic for any reason. This is to avoid DoS
46+
// vectors where a malicious actor crafts a transaction that panics the
47+
// prioritizer. Since the prioritizer is used as a hint only, it's safe to fall
48+
// back to zero priority in this case and log the panic for monitoring purposes.
49+
s.logger.Error("tx prioritizer panicked. Falling back on no priority", "error", r)
50+
_priorityHint = 0
51+
_err = nil
52+
}
53+
}()
54+
if ctx.HasPriority() {
55+
// The context already has a priority set, return it.
56+
return ctx.Priority(), nil
57+
}
58+
59+
if ok, err := evmante.IsEVMMessage(tx); err != nil {
60+
return 0, err
61+
} else if ok {
62+
evmTx := evmtypes.GetEVMTransactionMessage(tx)
63+
if evmTx != nil {
64+
return s.getEvmTxPriority(ctx, evmTx)
65+
}
66+
// This should never happen since IsEVMMessage returned true. But we defensively
67+
// return zero priority to be safe.
68+
return 0, nil
69+
}
70+
if feeTx, ok := tx.(sdk.FeeTx); ok {
71+
return s.getCosmosTxPriority(ctx, feeTx)
72+
}
73+
return 0, sdkerrors.Wrap(sdkerrors.ErrTxDecode, "Tx must either be EVM or Fee")
74+
}
75+
76+
func (s *SeiTxPrioritizer) getEvmTxPriority(ctx sdk.Context, evmTx *evmtypes.MsgEVMTransaction) (int64, error) {
77+
78+
// Unpack the transaction data first to avoid double unpacking as part of preprocessing.
79+
txData, err := evmtypes.UnpackTxData(evmTx.Data)
80+
if err != nil {
81+
return 0, err
82+
}
83+
84+
if err := evmante.PreprocessUnpacked(ctx, evmTx, s.evmKeeper.ChainID(ctx), s.evmKeeper.EthBlockTestConfig.Enabled, txData); err != nil {
85+
return 0, err
86+
}
87+
if evmTx.Derived.IsAssociate {
88+
_, isAssociated := s.evmKeeper.GetEVMAddress(
89+
ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)),
90+
evmTx.Derived.SenderSeiAddr)
91+
if !isAssociated {
92+
// Unassociated associate transactions have the second-highest priority.
93+
// This is to ensure that associate transactions are processed before
94+
// regular transactions, but after oracle transactions.
95+
//
96+
// Note that we are not checking if sufficient funds are present here to keep the
97+
// priority calculation fast. CheckTx should fully check the transaction.
98+
return antedecorators.EVMAssociatePriority, nil
99+
}
100+
return 0, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "account already has association set")
101+
}
102+
103+
// Check txData for sanity.
104+
feeCap := txData.GetGasFeeCap()
105+
fee := s.getEvmBaseFee(ctx)
106+
if feeCap.Cmp(fee) < 0 {
107+
return 0, sdkerrors.ErrInsufficientFee
108+
}
109+
minimumFee := s.evmKeeper.GetMinimumFeePerGas(ctx).TruncateInt().BigInt()
110+
if feeCap.Cmp(minimumFee) < 0 {
111+
return 0, sdkerrors.ErrInsufficientFee
112+
}
113+
if txData.GetGasTipCap().Sign() < 0 {
114+
return 0, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "gas fee cap cannot be negative")
115+
}
116+
// Check blob hashes for sanity. If EVM version is Cancun or later, and the
117+
// transaction contains at least one blob, we need to make sure the transaction
118+
// carries a non-zero blob fee cap.
119+
if evmTx.Derived != nil && evmTx.Derived.Version >= derived.Cancun && len(txData.GetBlobHashes()) > 0 {
120+
// For now we are simply assuming excessive blob gas is 0. In the future we might change it to be
121+
// dynamic based on prior block usage.
122+
chainConfig := evmtypes.DefaultChainConfig().EthereumConfig(s.evmKeeper.ChainID(ctx))
123+
if txData.GetBlobFeeCap().Cmp(eip4844.CalcBlobFee(chainConfig, &ethtypes.Header{Time: uint64(ctx.BlockTime().Unix())})) < 0 { //nolint:gosec
124+
return 0, sdkerrors.ErrInsufficientFee
125+
}
126+
}
127+
128+
gp := txData.EffectiveGasPrice(utils.Big0)
129+
priority := sdk.NewDecFromBigInt(gp).Quo(s.evmKeeper.GetPriorityNormalizer(ctx)).TruncateInt().BigInt()
130+
if priority.Cmp(big.NewInt(antedecorators.MaxPriority)) > 0 {
131+
priority = big.NewInt(antedecorators.MaxPriority)
132+
}
133+
return priority.Int64(), nil
134+
}
135+
136+
func (s *SeiTxPrioritizer) getEvmBaseFee(ctx sdk.Context) *big.Int {
137+
const (
138+
pacific1 = "pacific-1"
139+
historicalBlockHeight = 114945913
140+
doneHeightName = "6.2.0"
141+
)
142+
if ctx.ChainID() == pacific1 {
143+
height := ctx.BlockHeight()
144+
if height < historicalBlockHeight {
145+
return s.evmKeeper.GetBaseFeePerGas(ctx).TruncateInt().BigInt()
146+
}
147+
148+
doneHeight := s.upgradeKeeper.GetDoneHeight(
149+
ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)), doneHeightName)
150+
if height < doneHeight {
151+
return s.evmKeeper.GetCurrBaseFeePerGas(ctx).TruncateInt().BigInt()
152+
}
153+
}
154+
return s.evmKeeper.GetNextBaseFeePerGas(ctx).TruncateInt().BigInt()
155+
}
156+
157+
func (s *SeiTxPrioritizer) getCosmosTxPriority(ctx sdk.Context, feeTx sdk.FeeTx) (int64, error) {
158+
if isOracleTx(feeTx) {
159+
return antedecorators.OraclePriority, nil
160+
}
161+
162+
gas := feeTx.GetGas()
163+
if gas <= 0 {
164+
return 0, nil
165+
}
166+
var igas int64
167+
if gas > math.MaxInt64 {
168+
igas = math.MaxInt64
169+
} else {
170+
igas = int64(gas) //nolint:gosec
171+
}
172+
173+
feeParams := s.paramsKeeper.GetFeesParams(ctx)
174+
allowedDenoms := feeParams.GetAllowedFeeDenoms()
175+
denoms := make([]string, 0, len(allowedDenoms)+1)
176+
denoms = append(denoms, sdk.DefaultBondDenom)
177+
denoms = append(denoms, allowedDenoms...)
178+
feeCoins := feeTx.GetFee().NonZeroAmountsOf(denoms)
179+
priority := cosmosante.GetTxPriority(feeCoins, igas)
180+
return min(antedecorators.MaxPriority, priority), nil
181+
}
182+
183+
func isOracleTx(tx sdk.FeeTx) bool {
184+
if len(tx.GetMsgs()) == 0 {
185+
return false
186+
}
187+
for _, msg := range tx.GetMsgs() {
188+
switch msg.(type) {
189+
case *oracletypes.MsgAggregateExchangeRateVote:
190+
continue
191+
default:
192+
return false
193+
}
194+
}
195+
return true
196+
}

app/prioritizer_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package app_test
2+
3+
import (
4+
"testing"
5+
6+
cosmostypes "github.com/cosmos/cosmos-sdk/types"
7+
sdk "github.com/cosmos/cosmos-sdk/types"
8+
xparamtypes "github.com/cosmos/cosmos-sdk/x/params/types"
9+
"github.com/stretchr/testify/require"
10+
"github.com/stretchr/testify/suite"
11+
"github.com/tendermint/tendermint/libs/log"
12+
13+
"github.com/sei-protocol/sei-chain/app"
14+
"github.com/sei-protocol/sei-chain/app/antedecorators"
15+
"github.com/sei-protocol/sei-chain/app/apptesting"
16+
oracletypes "github.com/sei-protocol/sei-chain/x/oracle/types"
17+
)
18+
19+
type PrioritizerTestSuite struct {
20+
apptesting.KeeperTestHelper
21+
prioritizer *app.SeiTxPrioritizer
22+
}
23+
24+
func TestPrioritizerTestSuite(t *testing.T) {
25+
suite.Run(t, new(PrioritizerTestSuite))
26+
}
27+
28+
func (s *PrioritizerTestSuite) SetupTest() {
29+
s.KeeperTestHelper.Setup()
30+
logger, err := log.NewDefaultLogger(log.LogFormatPlain, "info")
31+
require.NoError(s.T(), err)
32+
s.prioritizer = app.NewSeiTxPrioritizer(logger, &s.App.EvmKeeper, &s.App.UpgradeKeeper, &s.App.ParamsKeeper)
33+
}
34+
35+
var (
36+
_ sdk.FeeTx = (*mockFeeTx)(nil)
37+
_ sdk.Tx = (*mockTx)(nil)
38+
)
39+
40+
type mockFeeTx struct {
41+
sdk.Tx
42+
fees sdk.Coins
43+
gas uint64
44+
msgs []sdk.Msg
45+
}
46+
47+
func (tx *mockFeeTx) FeePayer() sdk.AccAddress { return nil }
48+
func (tx *mockFeeTx) FeeGranter() sdk.AccAddress { return nil }
49+
func (tx *mockFeeTx) GetFee() sdk.Coins { return tx.fees }
50+
func (tx *mockFeeTx) GetGas() uint64 { return tx.gas }
51+
func (tx *mockFeeTx) GetMsgs() []sdk.Msg { return tx.msgs }
52+
53+
type mockTx struct {
54+
msgs []sdk.Msg
55+
gasEstimate uint64
56+
}
57+
58+
func (tx *mockTx) GetGasEstimate() uint64 { return tx.gasEstimate }
59+
func (tx *mockTx) GetMsgs() []sdk.Msg { return tx.msgs }
60+
func (*mockTx) ValidateBasic() error { return nil }
61+
func (*mockTx) GetSigners() []sdk.AccAddress { return nil }
62+
63+
func (s *PrioritizerTestSuite) TestGetTxPriority() {
64+
var (
65+
zeroValueTx = func(*PrioritizerTestSuite) sdk.Tx { return &mockTx{} }
66+
zeroValueFeeTx = func(*PrioritizerTestSuite) sdk.Tx { return &mockFeeTx{} }
67+
zeroGasFeeTx = func(*PrioritizerTestSuite) sdk.Tx {
68+
return &mockFeeTx{
69+
gas: 0,
70+
}
71+
}
72+
oracleVoteTx = func(s *PrioritizerTestSuite) sdk.Tx {
73+
return &mockFeeTx{
74+
msgs: []sdk.Msg{&oracletypes.MsgAggregateExchangeRateVote{}},
75+
}
76+
}
77+
)
78+
79+
for _, tc := range []struct {
80+
name string
81+
givenTx func(s *PrioritizerTestSuite) sdk.Tx
82+
givenContext func(sdk.Context) sdk.Context
83+
wantPriority int64
84+
wantErr string
85+
expectedErrAs interface{}
86+
}{
87+
{
88+
name: "unexpected Tx type is error",
89+
givenTx: zeroValueTx,
90+
wantErr: "must either be EVM or Fee",
91+
},
92+
{
93+
name: "context with priority present is context priority",
94+
givenTx: zeroValueFeeTx,
95+
givenContext: func(ctx sdk.Context) sdk.Context {
96+
return ctx.WithPriority(123)
97+
},
98+
wantPriority: 123,
99+
},
100+
{
101+
name: "oracle Tx type is oracle priority",
102+
givenTx: oracleVoteTx,
103+
wantPriority: antedecorators.OraclePriority,
104+
},
105+
{
106+
name: "zero gas FeeTx is zero priority",
107+
givenTx: zeroGasFeeTx,
108+
wantPriority: 0,
109+
},
110+
{
111+
name: "cosmos tx with denominators is has priority of smallest demon multiplier",
112+
givenTx: func(s *PrioritizerTestSuite) sdk.Tx {
113+
s.App.ParamsKeeper.SetFeesParams(s.Ctx, xparamtypes.FeesParams{
114+
AllowedFeeDenoms: []string{"fish", "lobster"},
115+
})
116+
return &mockFeeTx{
117+
gas: 4_200,
118+
fees: []sdk.Coin{
119+
{Denom: "fish", Amount: sdk.NewInt(230_000_000)},
120+
{Denom: "lobster", Amount: sdk.NewInt(290_000_000_000)},
121+
},
122+
}
123+
},
124+
wantPriority: cosmostypes.NewInt(230_000_000).QuoRaw(4_200).Int64(),
125+
},
126+
} {
127+
s.T().Run(tc.name, func(t *testing.T) {
128+
s.SetupTest()
129+
tx := tc.givenTx(s)
130+
ctx := s.Ctx
131+
if tc.givenContext != nil {
132+
ctx = tc.givenContext(ctx)
133+
}
134+
gotPriority, gotErr := s.prioritizer.GetTxPriorityHint(ctx, tx)
135+
if tc.wantErr != "" {
136+
require.Error(t, gotErr)
137+
require.ErrorContains(t, gotErr, tc.wantErr)
138+
} else {
139+
require.NoError(t, gotErr)
140+
require.Equal(t, tc.wantPriority, gotPriority)
141+
}
142+
})
143+
}
144+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ replace (
360360
github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.51
361361
// Latest goleveldb is broken, we have to stick to this version
362362
github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
363-
github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.6.4
363+
github.com/tendermint/tendermint => github.com/sei-protocol/sei-tendermint v0.6.5
364364
github.com/tendermint/tm-db => github.com/sei-protocol/tm-db v0.0.4
365365
golang.org/x/crypto => golang.org/x/crypto v0.31.0
366366
google.golang.org/grpc => google.golang.org/grpc v1.33.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,8 +1997,8 @@ github.com/sei-protocol/sei-iavl v0.2.0 h1:OisPjXiDT+oe+aeckzDEFgkZCYuUjHgs/PP8D
19971997
github.com/sei-protocol/sei-iavl v0.2.0/go.mod h1:qRf8QYUPfrAO7K6VDB2B2l/N7K5L76OorioGBcJBIbw=
19981998
github.com/sei-protocol/sei-ibc-go/v3 v3.3.6 h1:HHWvrslBpkXBHUFs+azwl36NuFEJyMo6huvsNPG854c=
19991999
github.com/sei-protocol/sei-ibc-go/v3 v3.3.6/go.mod h1:VwB/vWu4ysT5DN2aF78d17LYmx3omSAdq6gpKvM7XRA=
2000-
github.com/sei-protocol/sei-tendermint v0.6.4 h1:lMgzloTLo3ixNBHV+ETkUeh13fwOPqqdXZW3pZxQ8Bs=
2001-
github.com/sei-protocol/sei-tendermint v0.6.4/go.mod h1:SSZv0P1NBP/4uB3gZr5XJIan3ks3Ui8FJJzIap4r6uc=
2000+
github.com/sei-protocol/sei-tendermint v0.6.5 h1:6jJOw330mcK8Xu8PYiChByHpsl+yGujsl1WZXDW0G4Q=
2001+
github.com/sei-protocol/sei-tendermint v0.6.5/go.mod h1:SSZv0P1NBP/4uB3gZr5XJIan3ks3Ui8FJJzIap4r6uc=
20022002
github.com/sei-protocol/sei-tm-db v0.0.5 h1:3WONKdSXEqdZZeLuWYfK5hP37TJpfaUa13vAyAlvaQY=
20032003
github.com/sei-protocol/sei-tm-db v0.0.5/go.mod h1:Cpa6rGyczgthq7/0pI31jys2Fw0Nfrc+/jKdP1prVqY=
20042004
github.com/sei-protocol/tm-db v0.0.4 h1:7Y4EU62Xzzg6wKAHEotm7SXQR0aPLcGhKHkh3qd0tnk=

0 commit comments

Comments
 (0)