diff --git a/adapters/signer_extraction_adapter/signer_extraction_adapter.go b/adapters/signer_extraction_adapter/signer_extraction_adapter.go index e905c5b3..98ae04f9 100644 --- a/adapters/signer_extraction_adapter/signer_extraction_adapter.go +++ b/adapters/signer_extraction_adapter/signer_extraction_adapter.go @@ -12,6 +12,19 @@ type SignerData struct { Sequence uint64 } +// NewSignerData returns a new SignerData instance. +func NewSignerData(signer sdk.AccAddress, sequence uint64) SignerData { + return SignerData{ + Signer: signer, + Sequence: sequence, + } +} + +// String implements the fmt.Stringer interface. +func (s SignerData) String() string { + return fmt.Sprintf("SignerData{Signer: %s, Sequence: %d}", s.Signer, s.Sequence) +} + // SignerExtractionAdapter is an interface used to determine how the signers of a transaction should be extracted // from the transaction. type Adapter interface { diff --git a/block/base/abci.go b/block/base/abci.go index 3ac25394..1da77d0f 100644 --- a/block/base/abci.go +++ b/block/base/abci.go @@ -42,9 +42,26 @@ func (l *BaseLane) PrepareLane( ) } + // Get the transaction info for each transaction that was selected. + txsWithInfo := make([]utils.TxWithInfo, len(txsToInclude)) + for i, tx := range txsToInclude { + txInfo, err := l.GetTxInfo(ctx, tx) + if err != nil { + l.Logger().Error( + "failed to get tx info", + "lane", l.Name(), + "err", err, + ) + + return proposal, err + } + + txsWithInfo[i] = txInfo + } + // Update the proposal with the selected transactions. This fails if the lane attempted to add // more transactions than the allocated max block space for the lane. - if err := proposal.UpdateProposal(l, txsToInclude); err != nil { + if err := proposal.UpdateProposal(l, txsWithInfo); err != nil { l.Logger().Error( "failed to update proposal", "lane", l.Name(), @@ -102,8 +119,25 @@ func (l *BaseLane) ProcessLane( return proposal, err } + // Retrieve the transaction info for each transaction that belongs to the lane. + txsWithInfo := make([]utils.TxWithInfo, len(txsFromLane)) + for i, tx := range txsFromLane { + txInfo, err := l.GetTxInfo(ctx, tx) + if err != nil { + l.Logger().Error( + "failed to get tx info", + "lane", l.Name(), + "err", err, + ) + + return proposal, err + } + + txsWithInfo[i] = txInfo + } + // Optimistically update the proposal with the partial proposal. - if err := proposal.UpdateProposal(l, txsFromLane); err != nil { + if err := proposal.UpdateProposal(l, txsWithInfo); err != nil { l.Logger().Error( "failed to update proposal", "lane", l.Name(), diff --git a/block/base/handlers.go b/block/base/handlers.go index 702d038b..110a9a94 100644 --- a/block/base/handlers.go +++ b/block/base/handlers.go @@ -6,7 +6,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block/proposals" - "github.com/skip-mev/block-sdk/block/utils" ) // DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It @@ -27,7 +26,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler { for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() { tx := iterator.Tx() - txInfo, err := utils.GetTxInfo(l.TxEncoder(), tx) + txInfo, err := l.GetTxInfo(ctx, tx) if err != nil { l.Logger().Info("failed to get hash of tx", "err", err) diff --git a/block/base/mempool.go b/block/base/mempool.go index 0ec52675..17cf56d6 100644 --- a/block/base/mempool.go +++ b/block/base/mempool.go @@ -102,19 +102,24 @@ func NewMempool[C comparable](txPriority TxPriority[C], txEncoder sdk.TxEncoder, } } +// Priority returns the priority of the transaction. +func (cm *Mempool[C]) Priority(ctx sdk.Context, tx sdk.Tx) any { + return cm.txPriority.GetTxPriority(ctx, tx) +} + // Insert inserts a transaction into the mempool. func (cm *Mempool[C]) Insert(ctx context.Context, tx sdk.Tx) error { if err := cm.index.Insert(ctx, tx); err != nil { return fmt.Errorf("failed to insert tx into auction index: %w", err) } - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { cm.Remove(tx) return err } - cm.txCache[txInfo.Hash] = struct{}{} + cm.txCache[hash] = struct{}{} return nil } @@ -125,12 +130,12 @@ func (cm *Mempool[C]) Remove(tx sdk.Tx) error { return fmt.Errorf("failed to remove transaction from the mempool: %w", err) } - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { return fmt.Errorf("failed to get tx hash string: %w", err) } - delete(cm.txCache, txInfo.Hash) + delete(cm.txCache, hash) return nil } @@ -150,12 +155,12 @@ func (cm *Mempool[C]) CountTx() int { // Contains returns true if the transaction is contained in the mempool. func (cm *Mempool[C]) Contains(tx sdk.Tx) bool { - txInfo, err := utils.GetTxInfo(cm.txEncoder, tx) + hash, err := utils.GetTxHash(cm.txEncoder, tx) if err != nil { return false } - _, ok := cm.txCache[txInfo.Hash] + _, ok := cm.txCache[hash] return ok } diff --git a/block/base/tx_info.go b/block/base/tx_info.go new file mode 100644 index 00000000..3de609ec --- /dev/null +++ b/block/base/tx_info.go @@ -0,0 +1,41 @@ +package base + +import ( + "encoding/hex" + "fmt" + "strings" + + comettypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/block/utils" +) + +// GetTxInfo returns various information about the transaction that +// belongs to the lane including its priority, signer's, sequence number, +// size and more. +func (l *BaseLane) GetTxInfo(ctx sdk.Context, tx sdk.Tx) (utils.TxWithInfo, error) { + txBytes, err := l.cfg.TxEncoder(tx) + if err != nil { + return utils.TxWithInfo{}, fmt.Errorf("failed to encode transaction: %w", err) + } + + // TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc. + gasTx, ok := tx.(sdk.FeeTx) + if !ok { + return utils.TxWithInfo{}, fmt.Errorf("failed to cast transaction to gas tx") + } + + signers, err := l.cfg.SignerExtractor.GetSigners(tx) + if err != nil { + return utils.TxWithInfo{}, err + } + + return utils.TxWithInfo{ + Hash: strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBytes).Hash())), + Size: int64(len(txBytes)), + GasLimit: gasTx.GetGas(), + TxBytes: txBytes, + Priority: l.LaneMempool.Priority(ctx, tx), + Signers: signers, + }, nil +} diff --git a/block/lane.go b/block/lane.go index d0a1a619..7415547a 100644 --- a/block/lane.go +++ b/block/lane.go @@ -6,6 +6,7 @@ import ( sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/utils" ) // LaneMempool defines the interface a lane's mempool should implement. The basic API @@ -23,6 +24,9 @@ type LaneMempool interface { // Contains returns true if the transaction is contained in the mempool. Contains(tx sdk.Tx) bool + + // Priority returns the priority of a transaction that belongs to this lane. + Priority(ctx sdk.Context, tx sdk.Tx) any } // Lane defines an interface used for matching transactions to lanes, storing transactions, @@ -74,6 +78,11 @@ type Lane interface { // Match determines if a transaction belongs to this lane. Match(ctx sdk.Context, tx sdk.Tx) bool + + // GetTxInfo returns various information about the transaction that + // belongs to the lane including its priority, signer's, sequence number, + // size and more. + GetTxInfo(ctx sdk.Context, tx sdk.Tx) (utils.TxWithInfo, error) } // FindLane finds a Lanes from in an array of Lanes and returns it and its index if found. diff --git a/block/mocks/lane.go b/block/mocks/lane.go index 50288a67..8002e310 100644 --- a/block/mocks/lane.go +++ b/block/mocks/lane.go @@ -16,6 +16,8 @@ import ( proposals "github.com/skip-mev/block-sdk/block/proposals" types "github.com/cosmos/cosmos-sdk/types" + + utils "github.com/skip-mev/block-sdk/block/utils" ) // Lane is an autogenerated mock type for the Lane type @@ -105,6 +107,30 @@ func (_m *Lane) GetMaxBlockSpace() math.LegacyDec { return r0 } +// GetTxInfo provides a mock function with given fields: ctx, tx +func (_m *Lane) GetTxInfo(ctx types.Context, tx types.Tx) (utils.TxWithInfo, error) { + ret := _m.Called(ctx, tx) + + var r0 utils.TxWithInfo + var r1 error + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) (utils.TxWithInfo, error)); ok { + return rf(ctx, tx) + } + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) utils.TxWithInfo); ok { + r0 = rf(ctx, tx) + } else { + r0 = ret.Get(0).(utils.TxWithInfo) + } + + if rf, ok := ret.Get(1).(func(types.Context, types.Tx) error); ok { + r1 = rf(ctx, tx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Insert provides a mock function with given fields: _a0, _a1 func (_m *Lane) Insert(_a0 context.Context, _a1 types.Tx) error { ret := _m.Called(_a0, _a1) @@ -171,6 +197,22 @@ func (_m *Lane) PrepareLane(ctx types.Context, proposal proposals.Proposal, next return r0, r1 } +// Priority provides a mock function with given fields: ctx, tx +func (_m *Lane) Priority(ctx types.Context, tx types.Tx) interface{} { + ret := _m.Called(ctx, tx) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) interface{}); ok { + r0 = rf(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + // ProcessLane provides a mock function with given fields: ctx, proposal, txs, next func (_m *Lane) ProcessLane(ctx types.Context, proposal proposals.Proposal, txs []types.Tx, next block.ProcessLanesHandler) (proposals.Proposal, error) { ret := _m.Called(ctx, proposal, txs, next) diff --git a/block/mocks/lane_mempool.go b/block/mocks/lane_mempool.go index cecf96f4..7e0ca4ce 100644 --- a/block/mocks/lane_mempool.go +++ b/block/mocks/lane_mempool.go @@ -82,6 +82,22 @@ func (_m *LaneMempool) Insert(_a0 context.Context, _a1 types.Tx) error { return r0 } +// Priority provides a mock function with given fields: ctx, tx +func (_m *LaneMempool) Priority(ctx types.Context, tx types.Tx) interface{} { + ret := _m.Called(ctx, tx) + + var r0 interface{} + if rf, ok := ret.Get(0).(func(types.Context, types.Tx) interface{}); ok { + r0 = rf(ctx, tx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + // Remove provides a mock function with given fields: _a0 func (_m *LaneMempool) Remove(_a0 types.Tx) error { ret := _m.Called(_a0) diff --git a/block/proposals/proposals_test.go b/block/proposals/proposals_test.go index 3b0655cc..61a83810 100644 --- a/block/proposals/proposals_test.go +++ b/block/proposals/proposals_test.go @@ -9,10 +9,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + signerextraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" + "github.com/skip-mev/block-sdk/block/base" "github.com/skip-mev/block-sdk/block/mocks" "github.com/skip-mev/block-sdk/block/proposals" "github.com/skip-mev/block-sdk/block/proposals/types" "github.com/skip-mev/block-sdk/block/utils" + defaultlane "github.com/skip-mev/block-sdk/lanes/base" "github.com/skip-mev/block-sdk/testutils" ) @@ -29,7 +32,7 @@ func TestUpdateProposal(t *testing.T) { lane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe() t.Run("can update with no transactions", func(t *testing.T) { - proposal := proposals.NewProposal(log.NewNopLogger(), nil, 100, 100) + proposal := proposals.NewProposal(log.NewTestLogger(t), nil, 100, 100) err := proposal.UpdateProposal(lane, nil) require.NoError(t, err) @@ -61,9 +64,12 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) gasLimit := 100 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is not empty. @@ -107,9 +113,12 @@ func TestUpdateProposal(t *testing.T) { gasLimit += 100 } - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), gasLimit) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), gasLimit) + + txsWithInfo, err := getTxsWithInfo(txs) + require.NoError(t, err) - err = proposal.UpdateProposal(lane, txs) + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is not empty. @@ -144,9 +153,12 @@ func TestUpdateProposal(t *testing.T) { size := int64(len(txBzs[0])) gasLimit := uint64(100) - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), size, gasLimit) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), size, gasLimit) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) // Ensure that the proposal is empty. @@ -161,8 +173,11 @@ func TestUpdateProposal(t *testing.T) { otherlane.On("Name").Return("test").Maybe() otherlane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe() + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + // Attempt to add the same transaction again. - err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx}) + err = proposal.UpdateProposal(otherlane, txsWithInfo) require.Error(t, err) require.Equal(t, 1, len(proposal.Txs)) @@ -204,12 +219,18 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) + len(txBzs[1]) gasLimit := 200 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) require.NoError(t, err) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx2}) + err = proposal.UpdateProposal(lane, txsWithInfo) + require.NoError(t, err) + + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx2}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is not empty. @@ -242,14 +263,17 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) gasLimit := 100 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) lane := mocks.NewLane(t) lane.On("Name").Return("test").Maybe() lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -280,14 +304,17 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) gasLimit := 100 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)) lane := mocks.NewLane(t) lane.On("Name").Return("test").Maybe() lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe() - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) + + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -318,9 +345,12 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) gasLimit := 100 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size)-1, uint64(gasLimit)) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size)-1, uint64(gasLimit)) + + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -351,9 +381,12 @@ func TestUpdateProposal(t *testing.T) { size := len(txBzs[0]) gasLimit := 100 - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)-1) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)-1) + + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + err = proposal.UpdateProposal(lane, txsWithInfo) require.Error(t, err) // Ensure that the proposal is empty. @@ -392,16 +425,22 @@ func TestUpdateProposal(t *testing.T) { txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx, tx2}) require.NoError(t, err) - proposal := proposals.NewProposal(log.NewNopLogger(), encodingConfig.TxConfig.TxEncoder(), 10000, 10000) + proposal := proposals.NewProposal(log.NewTestLogger(t), encodingConfig.TxConfig.TxEncoder(), 10000, 10000) + + txsWithInfo, err := getTxsWithInfo([]sdk.Tx{tx}) + require.NoError(t, err) - err = proposal.UpdateProposal(lane, []sdk.Tx{tx}) + err = proposal.UpdateProposal(lane, txsWithInfo) require.NoError(t, err) otherlane := mocks.NewLane(t) otherlane.On("Name").Return("test2") otherlane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("1.0")) - err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx2}) + txsWithInfo, err = getTxsWithInfo([]sdk.Tx{tx2}) + require.NoError(t, err) + + err = proposal.UpdateProposal(otherlane, txsWithInfo) require.NoError(t, err) size := len(txBzs[0]) + len(txBzs[1]) @@ -540,3 +579,29 @@ func TestGetLaneLimits(t *testing.T) { }) } } + +func getTxsWithInfo(txs []sdk.Tx) ([]utils.TxWithInfo, error) { + encoding := testutils.CreateTestEncodingConfig() + + cfg := base.NewLaneConfig( + log.NewNopLogger(), + encoding.TxConfig.TxEncoder(), + encoding.TxConfig.TxDecoder(), + nil, + signerextraction.NewDefaultAdapter(), + math.LegacyNewDec(1), + ) + lane := defaultlane.NewDefaultLane(cfg) + + txsWithInfo := make([]utils.TxWithInfo, len(txs)) + for i, tx := range txs { + txInfo, err := lane.GetTxInfo(sdk.Context{}, tx) + if err != nil { + return nil, err + } + + txsWithInfo[i] = txInfo + } + + return txsWithInfo, nil +} diff --git a/block/proposals/update.go b/block/proposals/update.go index 0280586a..dfaf1858 100644 --- a/block/proposals/update.go +++ b/block/proposals/update.go @@ -4,15 +4,14 @@ import ( "fmt" "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/skip-mev/block-sdk/block/utils" ) // Lane defines the contract interface for a lane. type Lane interface { - GetMaxBlockSpace() math.LegacyDec Name() string + GetMaxBlockSpace() math.LegacyDec } // UpdateProposal updates the proposal with the given transactions and lane limits. There are a @@ -25,7 +24,7 @@ type Lane interface { // the lane. // 5. The lane must not have already prepared a partial proposal. // 6. The transaction must not already be in the proposal. -func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error { +func (p *Proposal) UpdateProposal(lane Lane, partialProposal []utils.TxWithInfo) error { if len(partialProposal) == 0 { return nil } @@ -42,29 +41,26 @@ func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error { partialProposalGasLimit := uint64(0) for index, tx := range partialProposal { - txInfo, err := utils.GetTxInfo(p.TxEncoder, tx) - if err != nil { - return fmt.Errorf("err retrieving transaction info: %s", err) - } - p.Logger.Info( "updating proposal with tx", - "index", index, + "index", index+len(p.Txs), "lane", lane.Name(), - "tx_hash", txInfo.Hash, - "tx_size", txInfo.Size, - "tx_gas_limit", txInfo.GasLimit, + "hash", tx.Hash, + "size", tx.Size, + "gas_limit", tx.GasLimit, + "signers", tx.Signers, + "priority", tx.Priority, ) // invariant check: Ensure that the transaction is not already in the proposal. - if _, ok := p.Cache[txInfo.Hash]; ok { - return fmt.Errorf("transaction %s is already in the proposal", txInfo.Hash) + if _, ok := p.Cache[tx.Hash]; ok { + return fmt.Errorf("transaction %s is already in the proposal", tx.Hash) } - hashes[txInfo.Hash] = struct{}{} - partialProposalSize += txInfo.Size - partialProposalGasLimit += txInfo.GasLimit - txs[index] = txInfo.TxBytes + hashes[tx.Hash] = struct{}{} + partialProposalSize += tx.Size + partialProposalGasLimit += tx.GasLimit + txs[index] = tx.TxBytes } // invariant check: Ensure that the partial proposal is not too large. diff --git a/block/utils/utils.go b/block/utils/utils.go index a0826f2e..c0ecd187 100644 --- a/block/utils/utils.go +++ b/block/utils/utils.go @@ -6,47 +6,62 @@ import ( "strings" comettypes "github.com/cometbft/cometbft/types" - sdk "github.com/cosmos/cosmos-sdk/types" sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool" + signerextraction "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter" ) -type ( - // TxInfo contains the information required for a transaction to be - // included in a proposal. - TxInfo struct { - // Hash is the hex-encoded hash of the transaction. - Hash string - // Size is the size of the transaction in bytes. - Size int64 - // GasLimit is the gas limit of the transaction. - GasLimit uint64 - // TxBytes is the bytes of the transaction. - TxBytes []byte - } -) +// TxWithInfo contains the information required for a transaction to be +// included in a proposal. +type TxWithInfo struct { + // Hash is the hex-encoded hash of the transaction. + Hash string + // Size is the size of the transaction in bytes. + Size int64 + // GasLimit is the gas limit of the transaction. + GasLimit uint64 + // TxBytes is the bytes of the transaction. + TxBytes []byte + // Priority defines the priority of the transaction. + Priority any + // Signers defines the signers of a transaction. + Signers []signerextraction.SignerData +} -// GetTxHashStr returns the TxInfo of a given transaction. -func GetTxInfo(txEncoder sdk.TxEncoder, tx sdk.Tx) (TxInfo, error) { - txBz, err := txEncoder(tx) - if err != nil { - return TxInfo{}, fmt.Errorf("failed to encode transaction: %w", err) +// NewTxInfo returns a new TxInfo instance. +func NewTxInfo( + hash string, + size int64, + gasLimit uint64, + txBytes []byte, + priority any, + signers []signerextraction.SignerData, +) TxWithInfo { + return TxWithInfo{ + Hash: hash, + Size: size, + GasLimit: gasLimit, + TxBytes: txBytes, + Priority: priority, + Signers: signers, } +} - txHashStr := strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBz).Hash())) +// String implements the fmt.Stringer interface. +func (t TxWithInfo) String() string { + return fmt.Sprintf("TxWithInfo{Hash: %s, Size: %d, GasLimit: %d, Priority: %s, Signers: %v}", + t.Hash, t.Size, t.GasLimit, t.Priority, t.Signers) +} - // TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc. - gasTx, ok := tx.(sdk.FeeTx) - if !ok { - return TxInfo{}, fmt.Errorf("failed to cast transaction to GasTx") +// GetTxHash returns the string hash representation of a transaction. +func GetTxHash(encoder sdk.TxEncoder, tx sdk.Tx) (string, error) { + txBz, err := encoder(tx) + if err != nil { + return "", fmt.Errorf("failed to encode transaction: %w", err) } - return TxInfo{ - Hash: txHashStr, - Size: int64(len(txBz)), - GasLimit: gasTx.GetGas(), - TxBytes: txBz, - }, nil + txHashStr := strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBz).Hash())) + return txHashStr, nil } // GetDecodedTxs returns the decoded transactions from the given bytes. diff --git a/lanes/base/abci_test.go b/lanes/base/abci_test.go index 02b1435c..ab9c6c28 100644 --- a/lanes/base/abci_test.go +++ b/lanes/base/abci_test.go @@ -510,7 +510,10 @@ func (s *BaseTestSuite) TestPrepareLane() { mockLane.On("Name").Return("test") mockLane.On("GetMaxBlockSpace").Return(math.LegacyOneDec()) - err = emptyProposal.UpdateProposal(mockLane, []sdk.Tx{tx}) + txWithInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + + err = emptyProposal.UpdateProposal(mockLane, []utils.TxWithInfo{txWithInfo}) s.Require().NoError(err) finalProposal, err := lane.PrepareLane(s.ctx, emptyProposal, block.NoOpPrepareLanesHandler()) diff --git a/lanes/base/tx_info_test.go b/lanes/base/tx_info_test.go new file mode 100644 index 00000000..a5abb069 --- /dev/null +++ b/lanes/base/tx_info_test.go @@ -0,0 +1,94 @@ +package base_test + +import ( + "math/rand" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/skip-mev/block-sdk/testutils" +) + +func (s *BaseTestSuite) TestGetTxInfo() { + accounts := testutils.RandomAccounts(rand.New(rand.NewSource(1)), 3) + lane := s.initLane(math.LegacyOneDec(), nil) + + s.Run("can retrieve information for a default tx", func() { + signer := accounts[0] + nonce := uint64(1) + fee := sdk.NewCoins(sdk.NewCoin(s.gasTokenDenom, math.NewInt(100))) + gasLimit := uint64(100) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + signer, + nonce, + 1, + 0, + gasLimit, + fee..., + ) + s.Require().NoError(err) + + txInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + s.Require().NotEmpty(txInfo.Hash) + + // Verify the signers + s.Require().Len(txInfo.Signers, 1) + s.Require().Equal(signer.Address.String(), txInfo.Signers[0].Signer.String()) + s.Require().Equal(nonce, txInfo.Signers[0].Sequence) + + // Verify the priority + actualfee, err := sdk.ParseCoinsNormalized(txInfo.Priority.(string)) + s.Require().NoError(err) + s.Require().Equal(fee, actualfee) + + // Verify the gas limit + s.Require().Equal(gasLimit, txInfo.GasLimit) + + // Verify the bytes + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + s.Require().Equal(txBz, txInfo.TxBytes) + + // Verify the size + s.Require().Equal(int64(len(txBz)), txInfo.Size) + }) + + s.Run("can retrieve information with different fees", func() { + signer := accounts[1] + nonce := uint64(10) + fee := sdk.NewCoins(sdk.NewCoin(s.gasTokenDenom, math.NewInt(20000))) + gasLimit := uint64(10000000) + + tx, err := testutils.CreateRandomTx( + s.encodingConfig.TxConfig, + signer, + nonce, + 10, + 0, + gasLimit, + fee..., + ) + s.Require().NoError(err) + + txInfo, err := lane.GetTxInfo(s.ctx, tx) + s.Require().NoError(err) + s.Require().NotEmpty(txInfo.Hash) + + // Verify the signers + s.Require().Len(txInfo.Signers, 1) + s.Require().Equal(signer.Address.String(), txInfo.Signers[0].Signer.String()) + s.Require().Equal(nonce, txInfo.Signers[0].Sequence) + + // Verify the priority + actualfee, err := sdk.ParseCoinsNormalized(txInfo.Priority.(string)) + s.Require().NoError(err) + s.Require().Equal(fee, actualfee) + + // Verify the bytes + txBz, err := s.encodingConfig.TxConfig.TxEncoder()(tx) + s.Require().NoError(err) + s.Require().Equal(txBz, txInfo.TxBytes) + }) +} diff --git a/lanes/mev/abci.go b/lanes/mev/abci.go index 5b03a605..5910f214 100644 --- a/lanes/mev/abci.go +++ b/lanes/mev/abci.go @@ -8,7 +8,6 @@ import ( "github.com/skip-mev/block-sdk/block/base" "github.com/skip-mev/block-sdk/block/proposals" - "github.com/skip-mev/block-sdk/block/utils" ) // PrepareLaneHandler will attempt to select the highest bid transaction that is valid @@ -35,7 +34,9 @@ func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { continue } - bundle, err := l.VerifyBidBasic(bidTx, proposal, limit) + cacheCtx, write := ctx.CacheContext() + + bundle, err := l.VerifyBidBasic(cacheCtx, bidTx, proposal, limit) if err != nil { l.Logger().Info( "failed to select auction bid tx for lane; tx is invalid", @@ -46,7 +47,6 @@ func (l *MEVLane) PrepareLaneHandler() base.PrepareLaneHandler { continue } - cacheCtx, write := ctx.CacheContext() if err := l.VerifyBidTx(cacheCtx, bidTx, bundle); err != nil { l.Logger().Info( "failed to select auction bid tx for lane; tx is invalid", @@ -161,6 +161,7 @@ func (l *MEVLane) ProcessLaneHandler() base.ProcessLaneHandler { // VerifyBidBasic will verify that the bid transaction and all of its bundled // transactions respect the basic invariants of the lane (e.g. size, gas limit). func (l *MEVLane) VerifyBidBasic( + ctx sdk.Context, bidTx sdk.Tx, proposal proposals.Proposal, limit proposals.LaneLimits, @@ -175,7 +176,7 @@ func (l *MEVLane) VerifyBidBasic( return nil, fmt.Errorf("bid info is nil") } - txInfo, err := utils.GetTxInfo(l.TxEncoder(), bidTx) + txInfo, err := l.GetTxInfo(ctx, bidTx) if err != nil { return nil, fmt.Errorf("err retrieving transaction info: %s", err) } @@ -196,7 +197,7 @@ func (l *MEVLane) VerifyBidBasic( return nil, fmt.Errorf("invalid bid tx; failed to decode bundled tx: %w", err) } - bundledTxInfo, err := utils.GetTxInfo(l.TxEncoder(), bundledTx) + bundledTxInfo, err := l.GetTxInfo(ctx, bundledTx) if err != nil { return nil, fmt.Errorf("err retrieving transaction info: %s", err) } diff --git a/lanes/mev/abci_test.go b/lanes/mev/abci_test.go index d8ddbaee..fee3c515 100644 --- a/lanes/mev/abci_test.go +++ b/lanes/mev/abci_test.go @@ -547,7 +547,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - bundle, err := lane.VerifyBidBasic(bidTx, proposal, limits) + bundle, err := lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().NoError(err) s.compare(bundle, expectedBundle) }) @@ -563,7 +563,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(tx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, tx, proposal, limits) s.Require().Error(err) }) @@ -579,7 +579,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) @@ -599,7 +599,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { proposal := proposals.NewProposal(log.NewNopLogger(), s.encCfg.TxConfig.TxEncoder(), size-1, 100) limits := proposal.GetLaneLimits(lane.GetMaxBlockSpace()) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) @@ -624,7 +624,7 @@ func (s *MEVTestSuite) TestVerifyBidBasic() { ) s.Require().NoError(err) - _, err = lane.VerifyBidBasic(bidTx, proposal, limits) + _, err = lane.VerifyBidBasic(s.ctx, bidTx, proposal, limits) s.Require().Error(err) }) } diff --git a/lanes/mev/mempool.go b/lanes/mev/mempool.go index cae04e62..a7e8cdcc 100644 --- a/lanes/mev/mempool.go +++ b/lanes/mev/mempool.go @@ -14,8 +14,8 @@ func TxPriority(config Factory) base.TxPriority[string] { return base.TxPriority[string]{ GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string { bidInfo, err := config.GetAuctionBidInfo(tx) - if err != nil { - panic(err) + if err != nil || bidInfo == nil || bidInfo.Bid.IsNil() { + return "" } return bidInfo.Bid.String() diff --git a/lanes/terminator/lane.go b/lanes/terminator/lane.go index c8e37273..e7d59a0c 100644 --- a/lanes/terminator/lane.go +++ b/lanes/terminator/lane.go @@ -11,6 +11,7 @@ import ( "github.com/skip-mev/block-sdk/block" "github.com/skip-mev/block-sdk/block/proposals" + "github.com/skip-mev/block-sdk/block/utils" ) const ( @@ -73,6 +74,11 @@ func (t Terminator) Name() string { return LaneName } +// GetTxInfo is a no-op +func (t Terminator) GetTxInfo(_ sdk.Context, _ sdk.Tx) (utils.TxWithInfo, error) { + return utils.TxWithInfo{}, fmt.Errorf("terminator lane should not have any transactions") +} + // SetAnteHandler is a no-op func (t Terminator) SetAnteHandler(sdk.AnteHandler) {} @@ -114,7 +120,12 @@ func (t Terminator) Select(context.Context, [][]byte) sdkmempool.Iterator { return nil } -// HasHigherPriority is a no-op +// Compare is a no-op func (t Terminator) Compare(sdk.Context, sdk.Tx, sdk.Tx) (int, error) { return 0, nil } + +// Priority is a no-op +func (t Terminator) Priority(sdk.Context, sdk.Tx) any { + return 0 +} diff --git a/tests/integration/block_sdk_suite.go b/tests/integration/block_sdk_suite.go index d67ba527..4e401abe 100644 --- a/tests/integration/block_sdk_suite.go +++ b/tests/integration/block_sdk_suite.go @@ -109,7 +109,8 @@ func (s *IntegrationTestSuite) SetupSubTest() { // query height height, err := s.chain.(*cosmos.CosmosChain).Height(context.Background()) require.NoError(s.T(), err) - WaitForHeight(s.T(), s.chain.(*cosmos.CosmosChain), height+1) + WaitForHeight(s.T(), s.chain.(*cosmos.CosmosChain), height+3) + s.T().Logf("reached height %d", height+2) } func (s *IntegrationTestSuite) TestQueryParams() { @@ -1354,7 +1355,7 @@ func (s *IntegrationTestSuite) TestNetwork() { } }) - amountToTest.Reset(5 * time.Minute) + amountToTest.Reset(3 * time.Minute) s.Run("can produce blocks with all types of transactions", func() { for { select { diff --git a/tests/integration/chain_setup.go b/tests/integration/chain_setup.go index c2929c38..f57f6575 100644 --- a/tests/integration/chain_setup.go +++ b/tests/integration/chain_setup.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "io" + "math/rand" "os" "path" "strings" @@ -183,7 +184,7 @@ func (s *IntegrationTestSuite) CreateDummyFreeTx( return Tx{ User: user, Msgs: []sdk.Msg{delegateMsg}, - GasPrice: 1000, + GasPrice: rand.Int63n(150000), SequenceIncrement: sequenceOffset, SkipInclusionCheck: true, IgnoreChecks: true,