rename of constructor to base

This commit is contained in:
David Terpay
2023-08-15 18:01:49 -04:00
parent bf4e3b8af2
commit af2b5226f3
22 changed files with 147 additions and 147 deletions
+452
View File
@@ -0,0 +1,452 @@
# 🎨 Base Lane
> 🏗️ Build your own lane in less than 10 minutes using the Base Lane
## 💡 Overview
The Base Lane is a generic implementation of a lane. It comes out of the
box with default implementations for all the required interfaces. It is meant to
be used as a starting point for building your own lane.
## 🤔 How to use it
> **Default Implementations**
>
> There are default implementations for all of the below which can be found in
> the `block/base` package. It is highly recommended that developers overview
> the default implementations before building their own lane.
There are **three** critical components to building a custom lane using the lane
constructor:
1. `LaneConfig` - The lane configuration which determines the basic properties
of the lane including the maximum block space that the lane can fill up.
2. `LaneMempool` - The lane mempool which is responsible for storing
transactions that have been verified and are waiting to be included in proposals.
3. `MatchHandler` - This is responsible for determining whether a transaction should
belong to this lane.
4. [**OPTIONAL**] `PrepareLaneHandler` - Allows developers to define their own
handler to customize the how transactions are verified and ordered before they
are included into a proposal.
5. [**OPTIONAL**] `CheckOrderHandler` - Allows developers to define their own
handler that will run any custom checks on whether transactions included in
block proposals are in the correct order (respecting the ordering rules of the
lane and the ordering rules of the other lanes).
6. [**OPTIONAL**] `ProcessLaneHandler` - Allows developers to define their own
handler for processing transactions that are included in block proposals.
### 1. 📝 Lane Config
The lane config (`LaneConfig`) is a simple configuration
object that defines the desired amount of block space the lane should
utilize when building a proposal, an antehandler that is used to verify
transactions as they are added/verified to/in a proposal, and more. By default,
we recommend that user's pass in all of the base apps configurations (txDecoder,
logger, etc.). A sample `LaneConfig` might look like the following:
```golang
config := block.LaneConfig{
Logger: app.Logger(),
TxDecoder: app.TxDecoder(),
TxEncoder: app.TxEncoder(),
AnteHandler: app.AnteHandler(),
MaxTxs: 0,
MaxBlockSpace: math.LegacyZeroDec(),
IgnoreList: []block.Lane{},
}
```
The three most important parameters to set are the `AnteHandler`, `MaxTxs`, and
`MaxBlockSpace`.
#### **AnteHandler**
With the default implementation, the `AnteHandler` is responsible for verifying
transactions as they are being considered for a new proposal or are being processed
in a proposed block. We recommend user's utilize the same antehandler chain that
is used in the base app. If developers want a certain `AnteDecorator` to be
ignored if it qualifies for a given lane, they can do so by using the `NewIgnoreDecorator`
defined in `block/utils/ante.go`.
For example, a free lane might want to ignore the `DeductFeeDecorator` so that it's
transactions are not charged any fees. Where ever the `AnteHandler` is defined,
we could add the following to ignore the `DeductFeeDecorator`:
```golang
anteDecorators := []sdk.AnteDecorator{
ante.NewSetUpContextDecorator(),
...,
utils.NewIgnoreDecorator(
ante.NewDeductFeeDecorator(
options.BaseOptions.AccountKeeper,
options.BaseOptions.BankKeeper,
options.BaseOptions.FeegrantKeeper,
options.BaseOptions.TxFeeChecker,
),
options.FreeLane,
),
...,
}
```
Anytime a transaction that qualifies for the free lane is being processed, the
`DeductFeeDecorator` will be ignored and no fees will be deducted!
#### **MaxTxs**
This sets the maximum number of transactions allowed in the mempool with
the semantics:
* if `MaxTxs` == 0, there is no cap on the number of transactions in the mempool
* if `MaxTxs` > 0, the mempool will cap the number of transactions it stores,
and will prioritize transactions by their priority and sender-nonce
(sequence number) when evicting transactions.
* if `MaxTxs` < 0, `Insert` is a no-op.
#### **MaxBlockSpace**
MaxBlockSpace is the maximum amount of block space that the lane will attempt to
fill when building a proposal. This parameter may be useful lanes that should be
limited (such as a free or onboarding lane) in space usage. Setting this to 0
will allow the lane to fill the block with as many transactions as possible.
If a block proposal request has a `MaxTxBytes` of 1000 and the lane has a
`MaxBlockSpace` of 0.5, the lane will attempt to fill the block with 500 bytes.
#### **[OPTIONAL] IgnoreList**
`IgnoreList` defines the list of lanes to ignore when processing transactions.
For example, say there are two lanes: default and free. The free lane is
processed after the default lane. In this case, the free lane should be added
to the ignore list of the default lane. Otherwise, the transactions that belong
to the free lane will be processed by the default lane (which accepts all
transactions by default).
### 2. 🗄️ LaneMempool
This is the data structure that is responsible for storing transactions
as they are being verified and are waiting to be included in proposals. `block/base/mempool.go`
provides an out-of-the-box implementation that should be used as a starting
point for building out the mempool and should cover most use cases. To
utilize the mempool, you must implement a `TxPriority[C]` struct that does the
following:
* Implements a `GetTxPriority` method that returns the priority (as defined
by the type `[C]`) of a given transaction.
* Implements a `Compare` method that returns the relative priority of two
transactions. If the first transaction has a higher priority, the method
should return -1, if the second transaction has a higher priority the method
should return 1, otherwise the method should return 0.
* Implements a `MinValue` method that returns the minimum priority value
that a transaction can have.
The default implementation can be found in `block/base/mempool.go`. What
if we wanted to prioritize transactions by the amount they have staked on a chain?
Well we could do something like the following:
```golang
// CustomTxPriority returns a TxPriority that prioritizes transactions by the
// amount they have staked on chain. This means that transactions with a higher
// amount staked will be prioritized over transactions with a lower amount staked.
func (p *CustomTxPriority) CustomTxPriority() TxPriority[string] {
return TxPriority[string]{
GetTxPriority: func(ctx context.Context, tx sdk.Tx) string {
// Get the signer of the transaction.
signer := p.getTransactionSigner(tx)
// Get the total amount staked by the signer on chain.
// This is abstracted away in the example, but you can
// implement this using the staking keeper.
totalStake, err := p.getTotalStake(ctx, signer)
if err != nil {
return ""
}
return totalStake.String()
},
Compare: func(a, b string) int {
aCoins, _ := sdk.ParseCoinsNormalized(a)
bCoins, _ := sdk.ParseCoinsNormalized(b)
switch {
case aCoins == nil && bCoins == nil:
return 0
case aCoins == nil:
return -1
case bCoins == nil:
return 1
default:
switch {
case aCoins.IsAllGT(bCoins):
return 1
case aCoins.IsAllLT(bCoins):
return -1
default:
return 0
}
}
},
MinValue: "",
}
}
```
#### Using a Custom TxPriority
To utilize this new priority configuration in a lane, all you have to then do
is pass in the `TxPriority[C]` to the `NewLaneMempool` function.
```golang
// Create the lane config
laneCfg := NewLaneConfig(
...
MaxTxs: 100,
...
)
// Pseudocode for creating the custom tx priority
priorityCfg := NewPriorityConfig(
stakingKeeper,
accountKeeper,
...
)
// define your mempool that orders transactions by on-chain stake
mempool := constructor.NewMempool[string](
priorityCfg.CustomTxPriority(),
laneCfg.TxEncoder,
laneCfg.MaxTxs,
)
// Initialize your lane with the mempool
lane := constructor.NewBaseLane(
laneCfg,
LaneName,
mempool,
constructor.DefaultMatchHandler(),
)
```
### 3. 🤝 MatchHandler
`MatchHandler` is utilized to determine if a transaction should be included in
the lane. This function can be a stateless or stateful check on the transaction.
The default implementation can be found in `block/base/handlers.go`.
The match handler can be as custom as desired. Following the example above, if
we wanted to make a lane that only accepts transactions if they have a large
amount staked, we could do the following:
```golang
// CustomMatchHandler returns a custom implementation of the MatchHandler. It
// matches transactions that have a large amount staked. These transactions
// will then be charged no fees at execution time.
//
// NOTE: This is a stateful check on the transaction. The details of how to
// implement this are abstracted away in the example, but you can implement
// this using the staking keeper.
func (h *Handler) CustomMatchHandler() block.MatchHandler {
return func(ctx sdk.Context, tx sdk.Tx) bool {
if !h.IsStakingTx(tx) {
return false
}
signer, err := getTxSigner(tx)
if err != nil {
return false
}
stakedAmount, err := h.GetStakedAmount(signer)
if err != nil {
return false
}
// The transaction can only be considered for inclusion if the amount
// staked is greater than some predetermined threshold.
return stakeAmount.GT(h.Threshold)
}
}
```
#### Using a Custom MatchHandler
If we wanted to create the lane using the custom match handler along with the
custom mempool, we could do the following:
```golang
// Pseudocode for creating the custom match handler
handler := NewHandler(
stakingKeeper,
accountKeeper,
...
)
// define your mempool that orders transactions by on chain stake
mempool := constructor.NewMempool[string](
priorityCfg.CustomTxPriority(),
cfg.TxEncoder,
cfg.MaxTxs,
)
// Initialize your lane with the mempool
lane := constructor.NewBaseLane(
cfg,
LaneName,
mempool,
handler.CustomMatchHandler(),
)
```
### Summary on Steps 1-3
The following is a summary of the steps above:
1. Create a custom `LaneConfig` struct that defines the configuration of the lane.
2. Create a custom `TxPriority[C]` struct to have a custom mempool that orders
transactions via a custom priority mechanism.
3. Create a custom `MatchHandler` that implements the `block.MatchHandler` to
have a custom lane that only accepts transactions that match a custom criteria.
### [OPTIONAL] Steps 4-6
The remaining steps walk through the process of creating custom block
building/verification logic. The default implementation found in `block/base/handlers.go`
should fit most use cases. Please reference that file for more details on
the default implementation and whether it fits your use case.
Implementing custom block building/verification logic is a bit more involved
than the previous steps and is a all or nothing approach. This means that if
you implement any of the handlers, you must implement all of them in most cases.
If you do not implement all of them, the lane may have unintended behavior.
### 4. 🛠️ PrepareLaneHandler
The `PrepareLaneHandler` is an optional field you can set on the lane constructor.
This handler is responsible for the transaction selection logic when a new proposal
is requested.
The handler should return the following for a given lane:
1. The transactions to be included in the block proposal.
2. The transactions to be removed from the lane's mempool.
3. An error if the lane is unable to prepare a block proposal.
```golang
// PrepareLaneHandler is responsible for preparing transactions to be included
// in the block from a given lane. Given a lane, this function should return
// the transactions to include in the block, the transactions that must be
// removed from the lane, and an error if one occurred.
PrepareLaneHandler func(ctx sdk.Context,proposal BlockProposal,maxTxBytes int64)
(txsToInclude [][]byte, txsToRemove []sdk.Tx, err error)
```
The default implementation is simple. It will continue to select transactions
from its mempool under the following criteria:
1. The transactions is not already included in the block proposal.
2. The transaction is valid and passes the AnteHandler check.
3. The transaction is not too large to be included in the block.
If a more involved selection process is required, you can implement your own
`PrepareLaneHandler` and and set it after creating the lane constructor.
```golang
// Pseudocode for creating the custom prepare lane handler
// This assumes that the CustomLane inherits from the constructor
// lane.
customLane := constructor.NewCustomLane(
cfg,
LaneName,
mempool,
handler.CustomMatchHandler(),
)
// Set the custom PrepareLaneHandler on the lane
customLane.SetPrepareLaneHandler(customlane.PrepareLaneHandler())
```
### 5. ✅ CheckOrderHandler
The `CheckOrderHandler` is an optional field you can set on the lane constructor.
This handler is responsible for verifying the ordering of the transactions in the
block proposal that belong to the lane.
```golang
// CheckOrderHandler is responsible for checking the order of transactions that
// belong to a given lane. This handler should be used to verify that the
// ordering of transactions passed into the function respect the ordering logic
// of the lane (if any transactions from the lane are included). This function
// should also ensure that transactions that belong to this lane are contiguous
// and do not have any transactions from other lanes in between them.
CheckOrderHandler func(ctx sdk.Context, txs []sdk.Tx) error
```
The default implementation is simple and utilizes the same `TxPriority` struct
that the mempool uses to determine if transactions are in order. The criteria
for determining if transactions are in order is as follows:
1. The transactions are in order according to the `TxPriority` struct. i.e. any
two transactions (that match to the lane) `tx1` and `tx2` where `tx1` has a
higher priority than `tx2` should be ordered before `tx2`.
2. The transactions are contiguous. i.e. there are no transactions from other
lanes in between the transactions that belong to this lane. i.e. if `tx1` and
`tx2` belong to the lane, there should be no transactions from other lanes in
between `tx1` and `tx2`.
If a more involved ordering process is required, you can implement your own
`CheckOrderHandler` and and set it after creating the lane constructor.
```golang
// Pseudocode for creating the custom check order handler
// This assumes that the CustomLane inherits from the constructor
// lane.
customLane := constructor.NewCustomLane(
cfg,
LaneName,
mempool,
handler.CustomMatchHandler(),
)
// Set the custom CheckOrderHandler on the lane
customLane.SetCheckOrderHandler(customlane.CheckOrderHandler())
```
### 6. 🆗 ProcessLaneHandler
The `ProcessLaneHandler` is an optional field you can set on the lane constructor.
This handler is responsible for verifying the transactions in the block proposal
that belong to the lane. This handler is executed after the `CheckOrderHandler`
so the transactions passed into this function SHOULD already be in order
respecting the ordering rules of the lane and respecting the ordering rules of
mempool relative to the lanes it has. This means that if the first transaction
does not belong to the lane, the remaining transactions should not belong to the
lane either.
```golang
// ProcessLaneHandler is responsible for processing transactions that are
// included in a block and belong to a given lane. ProcessLaneHandler is
// executed after CheckOrderHandler so the transactions passed into this
// function SHOULD already be in order respecting the ordering rules of the
// lane and respecting the ordering rules of mempool relative to the lanes it has.
ProcessLaneHandler func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error)
```
Given the invarients above, the default implementation is simple. It will
continue to verify transactions in the block proposal under the following
criteria:
1. If a transaction matches to this lane, verify it and continue. If it is not
valid, return an error.
2. If a transaction does not match to this lane, return the remaining transactions
to the next lane to process.
+68
View File
@@ -0,0 +1,68 @@
package base
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/block"
"github.com/skip-mev/pob/block/utils"
)
// PrepareLane will prepare a partial proposal for the lane. It will select transactions from the
// lane respecting the selection logic of the prepareLaneHandler. It will then update the partial
// proposal with the selected transactions. If the proposal is unable to be updated, we return an
// error. The proposal will only be modified if it passes all of the invarient checks.
func (l *BaseLane) PrepareLane(
ctx sdk.Context,
proposal block.BlockProposal,
maxTxBytes int64,
next block.PrepareLanesHandler,
) (block.BlockProposal, error) {
txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes)
if err != nil {
return proposal, err
}
// Remove all transactions that were invalid during the creation of the partial proposal.
if err := utils.RemoveTxsFromLane(txsToRemove, l); err != nil {
l.Logger().Error(
"failed to remove transactions from lane",
"lane", l.Name(),
"err", err,
)
}
// Update the proposal with the selected transactions.
if err := proposal.UpdateProposal(l, txs); err != nil {
return proposal, err
}
return next(ctx, proposal)
}
// CheckOrder checks that the ordering logic of the lane is respected given the set of transactions
// in the block proposal. If the ordering logic is not respected, we return an error.
func (l *BaseLane) CheckOrder(ctx sdk.Context, txs []sdk.Tx) error {
return l.checkOrderHandler(ctx, txs)
}
// ProcessLane verifies that the transactions included in the block proposal are valid respecting
// the verification logic of the lane (processLaneHandler). If the transactions are valid, we
// return the transactions that do not belong to this lane to the next lane. If the transactions
// are invalid, we return an error.
func (l *BaseLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next block.ProcessLanesHandler) (sdk.Context, error) {
remainingTxs, err := l.processLaneHandler(ctx, txs)
if err != nil {
return ctx, err
}
return next(ctx, remainingTxs)
}
// AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of
// of the antehandler chain.
func (l *BaseLane) AnteVerifyTx(ctx sdk.Context, tx sdk.Tx, simulate bool) (sdk.Context, error) {
if l.cfg.AnteHandler != nil {
return l.cfg.AnteHandler(ctx, tx, simulate)
}
return ctx, nil
}
+79
View File
@@ -0,0 +1,79 @@
package base
import (
"fmt"
"cosmossdk.io/log"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/block"
)
// LaneConfig defines the basic functionality needed for a lane.
type LaneConfig struct {
Logger log.Logger
TxEncoder sdk.TxEncoder
TxDecoder sdk.TxDecoder
AnteHandler sdk.AnteHandler
// MaxBlockSpace defines the relative percentage of block space that can be
// used by this lane. NOTE: If this is set to zero, then there is no limit
// on the number of transactions that can be included in the block for this
// lane (up to maxTxBytes as provided by the request). This is useful for the default lane.
MaxBlockSpace math.LegacyDec
// IgnoreList defines the list of lanes to ignore when processing transactions. This
// is useful for when you want lanes to exist after the default lane. For example,
// say there are two lanes: default and free. The free lane should be processed after
// the default lane. In this case, the free lane should be added to the ignore list
// of the default lane. Otherwise, the transactions that belong to the free lane
// will be processed by the default lane (which accepts all transactions by default).
IgnoreList []block.Lane
// MaxTxs sets the maximum number of transactions allowed in the mempool with
// the semantics:
// - if MaxTx == 0, there is no cap on the number of transactions in the mempool
// - if MaxTx > 0, the mempool will cap the number of transactions it stores,
// and will prioritize transactions by their priority and sender-nonce
// (sequence number) when evicting transactions.
// - if MaxTx < 0, `Insert` is a no-op.
MaxTxs int
}
// NewLaneConfig returns a new LaneConfig. This will be embedded in a lane.
func NewLaneConfig(
logger log.Logger,
txEncoder sdk.TxEncoder,
txDecoder sdk.TxDecoder,
anteHandler sdk.AnteHandler,
maxBlockSpace math.LegacyDec,
) LaneConfig {
return LaneConfig{
Logger: logger,
TxEncoder: txEncoder,
TxDecoder: txDecoder,
AnteHandler: anteHandler,
MaxBlockSpace: maxBlockSpace,
}
}
// ValidateBasic validates the lane configuration.
func (c *LaneConfig) ValidateBasic() error {
if c.Logger == nil {
return fmt.Errorf("logger cannot be nil")
}
if c.TxEncoder == nil {
return fmt.Errorf("tx encoder cannot be nil")
}
if c.TxDecoder == nil {
return fmt.Errorf("tx decoder cannot be nil")
}
if c.MaxBlockSpace.IsNil() || c.MaxBlockSpace.IsNegative() || c.MaxBlockSpace.GT(math.LegacyOneDec()) {
return fmt.Errorf("max block space must be set to a value between 0 and 1")
}
return nil
}
+156
View File
@@ -0,0 +1,156 @@
package base
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/block"
"github.com/skip-mev/pob/block/utils"
)
// DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It
// selects all transactions in the mempool that are valid and not already in the partial
// proposal. It will continue to reap transactions until the maximum block space for this
// lane has been reached. Additionally, any transactions that are invalid will be returned.
func (l *BaseLane) DefaultPrepareLaneHandler() block.PrepareLaneHandler {
return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) {
var (
totalSize int64
txs [][]byte
txsToRemove []sdk.Tx
)
// Select all transactions in the mempool that are valid and not already in the
// partial proposal.
for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
tx := iterator.Tx()
txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx)
if err != nil {
l.Logger().Info("failed to get hash of tx", "err", err)
txsToRemove = append(txsToRemove, tx)
continue
}
// Double check that the transaction belongs to this lane.
if !l.Match(ctx, tx) {
l.Logger().Info(
"failed to select tx for lane; tx does not belong to lane",
"tx_hash", hash,
"lane", l.Name(),
)
txsToRemove = append(txsToRemove, tx)
continue
}
// if the transaction is already in the (partial) block proposal, we skip it.
if proposal.Contains(txBytes) {
l.Logger().Info(
"failed to select tx for lane; tx is already in proposal",
"tx_hash", hash,
"lane", l.Name(),
)
continue
}
// If the transaction is too large, we break and do not attempt to include more txs.
txSize := int64(len(txBytes))
if updatedSize := totalSize + txSize; updatedSize > maxTxBytes {
l.Logger().Info(
"tx bytes above the maximum allowed",
"lane", l.Name(),
"tx_size", txSize,
"total_size", totalSize,
"max_tx_bytes", maxTxBytes,
"tx_hash", hash,
)
break
}
// Verify the transaction.
if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil {
l.Logger().Info(
"failed to verify tx",
"tx_hash", hash,
"err", err,
)
txsToRemove = append(txsToRemove, tx)
continue
}
totalSize += txSize
txs = append(txs, txBytes)
}
return txs, txsToRemove, nil
}
}
// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It
// verifies all transactions in the lane that matches to the lane. If any transaction
// fails to verify, the entire proposal is rejected. If the handler comes across a transaction
// that does not match the lane's matcher, it will return the remaining transactions in the
// proposal.
func (l *BaseLane) DefaultProcessLaneHandler() block.ProcessLaneHandler {
return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) {
var err error
// Process all transactions that match the lane's matcher.
for index, tx := range txs {
if l.Match(ctx, tx) {
if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil {
return nil, fmt.Errorf("failed to verify tx: %w", err)
}
} else {
return txs[index:], nil
}
}
// This means we have processed all transactions in the proposal.
return nil, nil
}
}
// DefaultCheckOrderHandler returns a default implementation of the CheckOrderHandler. It
// ensures the following invariants:
//
// 1. All transactions that belong to this lane respect the ordering logic defined by the
// lane.
// 2. Transactions that belong to other lanes cannot be interleaved with transactions that
// belong to this lane.
func (l *BaseLane) DefaultCheckOrderHandler() block.CheckOrderHandler {
return func(ctx sdk.Context, txs []sdk.Tx) error {
seenOtherLaneTx := false
for index, tx := range txs {
if l.Match(ctx, tx) {
if seenOtherLaneTx {
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
}
// If the transactions do not respect the priority defined by the mempool, we consider the proposal
// to be invalid
if index > 0 && l.Compare(ctx, txs[index-1], tx) == -1 {
return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
}
} else {
seenOtherLaneTx = true
}
}
return nil
}
}
// DefaultMatchHandler returns a default implementation of the MatchHandler. It matches all
// transactions.
func DefaultMatchHandler() block.MatchHandler {
return func(ctx sdk.Context, tx sdk.Tx) bool {
return true
}
}
+199
View File
@@ -0,0 +1,199 @@
package base
import (
"fmt"
"cosmossdk.io/log"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/pob/block"
)
var _ block.Lane = (*BaseLane)(nil)
// BaseLane is a generic implementation of a lane. It is meant to be used
// as a base for other lanes to be built on top of. It provides a default
// implementation of the MatchHandler, PrepareLaneHandler, ProcessLaneHandler,
// and CheckOrderHandler. To extend this lane, you must either utilize the default
// handlers or construct your own that you pass into the base/setters.
type BaseLane struct {
// cfg stores functionality required to encode/decode transactions, maintains how
// many transactions are allowed in this lane's mempool, and the amount of block
// space this lane is allowed to consume.
cfg LaneConfig
// laneName is the name of the lane.
laneName string
// LaneMempool is the mempool that is responsible for storing transactions
// that are waiting to be processed.
block.LaneMempool
// matchHandler is the function that determines whether or not a transaction
// should be processed by this lane.
matchHandler block.MatchHandler
// prepareLaneHandler is the function that is called when a new proposal is being
// requested and the lane needs to submit transactions it wants included in the block.
prepareLaneHandler block.PrepareLaneHandler
// checkOrderHandler is the function that is called when a new proposal is being
// verified and the lane needs to verify that the transactions included in the proposal
// respect the ordering rules of the lane and does not interleave transactions from other lanes.
checkOrderHandler block.CheckOrderHandler
// processLaneHandler is the function that is called when a new proposal is being
// verified and the lane needs to verify that the transactions included in the proposal
// are valid respecting the verification logic of the lane.
processLaneHandler block.ProcessLaneHandler
}
// NewBaseLane returns a new lane base. When creating this lane, the type
// of the lane must be specified. The type of the lane is directly associated with the
// type of the mempool that is used to store transactions that are waiting to be processed.
func NewBaseLane(
cfg LaneConfig,
laneName string,
laneMempool block.LaneMempool,
matchHandlerFn block.MatchHandler,
) *BaseLane {
lane := &BaseLane{
cfg: cfg,
laneName: laneName,
LaneMempool: laneMempool,
matchHandler: matchHandlerFn,
}
if err := lane.ValidateBasic(); err != nil {
panic(err)
}
return lane
}
// ValidateBasic ensures that the lane was constructed properly. In the case that
// the lane was not constructed with proper handlers, default handlers are set.
func (l *BaseLane) ValidateBasic() error {
if err := l.cfg.ValidateBasic(); err != nil {
return err
}
if l.laneName == "" {
return fmt.Errorf("lane name cannot be empty")
}
if l.LaneMempool == nil {
return fmt.Errorf("lane mempool cannot be nil")
}
if l.matchHandler == nil {
return fmt.Errorf("match handler cannot be nil")
}
if l.prepareLaneHandler == nil {
l.prepareLaneHandler = l.DefaultPrepareLaneHandler()
}
if l.processLaneHandler == nil {
l.processLaneHandler = l.DefaultProcessLaneHandler()
}
if l.checkOrderHandler == nil {
l.checkOrderHandler = l.DefaultCheckOrderHandler()
}
return nil
}
// SetPrepareLaneHandler sets the prepare lane handler for the lane. This handler
// is called when a new proposal is being requested and the lane needs to submit
// transactions it wants included in the block.
func (l *BaseLane) SetPrepareLaneHandler(prepareLaneHandler block.PrepareLaneHandler) {
if prepareLaneHandler == nil {
panic("prepare lane handler cannot be nil")
}
l.prepareLaneHandler = prepareLaneHandler
}
// SetProcessLaneHandler sets the process lane handler for the lane. This handler
// is called when a new proposal is being verified and the lane needs to verify
// that the transactions included in the proposal are valid respecting the verification
// logic of the lane.
func (l *BaseLane) SetProcessLaneHandler(processLaneHandler block.ProcessLaneHandler) {
if processLaneHandler == nil {
panic("process lane handler cannot be nil")
}
l.processLaneHandler = processLaneHandler
}
// SetCheckOrderHandler sets the check order handler for the lane. This handler
// is called when a new proposal is being verified and the lane needs to verify
// that the transactions included in the proposal respect the ordering rules of
// the lane and does not include transactions from other lanes.
func (l *BaseLane) SetCheckOrderHandler(checkOrderHandler block.CheckOrderHandler) {
if checkOrderHandler == nil {
panic("check order handler cannot be nil")
}
l.checkOrderHandler = checkOrderHandler
}
// Match returns true if the transaction should be processed by this lane. This
// function first determines if the transaction matches the lane and then checks
// if the transaction is on the ignore list. If the transaction is on the ignore
// list, it returns false.
func (l *BaseLane) Match(ctx sdk.Context, tx sdk.Tx) bool {
return l.matchHandler(ctx, tx) && !l.CheckIgnoreList(ctx, tx)
}
// CheckIgnoreList returns true if the transaction is on the ignore list. The ignore
// list is utilized to prevent transactions that should be considered in other lanes
// from being considered from this lane.
func (l *BaseLane) CheckIgnoreList(ctx sdk.Context, tx sdk.Tx) bool {
for _, lane := range l.cfg.IgnoreList {
if lane.Match(ctx, tx) {
return true
}
}
return false
}
// Name returns the name of the lane.
func (l *BaseLane) Name() string {
return l.laneName
}
// SetIgnoreList sets the ignore list for the lane. The ignore list is a list
// of lanes that the lane should ignore when processing transactions.
func (l *BaseLane) SetIgnoreList(lanes []block.Lane) {
l.cfg.IgnoreList = lanes
}
// SetAnteHandler sets the ante handler for the lane.
func (l *BaseLane) SetAnteHandler(anteHandler sdk.AnteHandler) {
l.cfg.AnteHandler = anteHandler
}
// Logger returns the logger for the lane.
func (l *BaseLane) Logger() log.Logger {
return l.cfg.Logger
}
// TxDecoder returns the tx decoder for the lane.
func (l *BaseLane) TxDecoder() sdk.TxDecoder {
return l.cfg.TxDecoder
}
// TxEncoder returns the tx encoder for the lane.
func (l *BaseLane) TxEncoder() sdk.TxEncoder {
return l.cfg.TxEncoder
}
// GetMaxBlockSpace returns the maximum amount of block space that the lane is
// allowed to consume as a percentage of the total block space.
func (l *BaseLane) GetMaxBlockSpace() math.LegacyDec {
return l.cfg.MaxBlockSpace
}
+159
View File
@@ -0,0 +1,159 @@
package base
import (
"context"
"errors"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/skip-mev/pob/block/utils"
)
type (
// ConstructorMempool defines a mempool that orders transactions based on the
// txPriority. The mempool is a wrapper on top of the SDK's Priority Nonce mempool.
// It include's additional helper functions that allow users to determine if a
// transaction is already in the mempool and to compare the priority of two
// transactions.
Mempool[C comparable] struct {
// index defines an index of transactions.
index sdkmempool.Mempool
// txPriority defines the transaction priority function. It is used to
// retrieve the priority of a given transaction and to compare the priority
// of two transactions. The index utilizes this struct to order transactions
// in the mempool.
txPriority TxPriority[C]
// txEncoder defines the sdk.Tx encoder that allows us to encode transactions
// to bytes.
txEncoder sdk.TxEncoder
// txCache is a map of all transactions in the mempool. It is used
// to quickly check if a transaction is already in the mempool.
txCache map[string]struct{}
}
)
// DefaultTxPriority returns a default implementation of the TxPriority. It prioritizes
// transactions by their fee.
func DefaultTxPriority() TxPriority[string] {
return TxPriority[string]{
GetTxPriority: func(goCtx context.Context, tx sdk.Tx) string {
feeTx, ok := tx.(sdk.FeeTx)
if !ok {
return ""
}
return feeTx.GetFee().String()
},
Compare: func(a, b string) int {
aCoins, _ := sdk.ParseCoinsNormalized(a)
bCoins, _ := sdk.ParseCoinsNormalized(b)
switch {
case aCoins == nil && bCoins == nil:
return 0
case aCoins == nil:
return -1
case bCoins == nil:
return 1
default:
switch {
case aCoins.IsAllGT(bCoins):
return 1
case aCoins.IsAllLT(bCoins):
return -1
default:
return 0
}
}
},
MinValue: "",
}
}
// NewMempool returns a new ConstructorMempool.
func NewMempool[C comparable](txPriority TxPriority[C], txEncoder sdk.TxEncoder, maxTx int) *Mempool[C] {
return &Mempool[C]{
index: NewPriorityMempool(
PriorityNonceMempoolConfig[C]{
TxPriority: txPriority,
MaxTx: maxTx,
},
),
txPriority: txPriority,
txEncoder: txEncoder,
txCache: make(map[string]struct{}),
}
}
// 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)
}
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
if err != nil {
cm.Remove(tx)
return err
}
cm.txCache[txHashStr] = struct{}{}
return nil
}
// Remove removes a transaction from the mempool.
func (cm *Mempool[C]) Remove(tx sdk.Tx) error {
if err := cm.index.Remove(tx); err != nil && !errors.Is(err, sdkmempool.ErrTxNotFound) {
return fmt.Errorf("failed to remove transaction from the mempool: %w", err)
}
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
if err != nil {
return fmt.Errorf("failed to get tx hash string: %w", err)
}
delete(cm.txCache, txHashStr)
return nil
}
// Select returns an iterator of all transactions in the mempool. NOTE: If you
// remove a transaction from the mempool while iterating over the transactions,
// the iterator will not be aware of the removal and will continue to iterate
// over the removed transaction. Be sure to reset the iterator if you remove a transaction.
func (cm *Mempool[C]) Select(ctx context.Context, txs [][]byte) sdkmempool.Iterator {
return cm.index.Select(ctx, txs)
}
// CountTx returns the number of transactions in the mempool.
func (cm *Mempool[C]) CountTx() int {
return cm.index.CountTx()
}
// Contains returns true if the transaction is contained in the mempool.
func (cm *Mempool[C]) Contains(tx sdk.Tx) bool {
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
if err != nil {
return false
}
_, ok := cm.txCache[txHashStr]
return ok
}
// Compare determines the relative priority of two transactions belonging in the same lane.
func (cm *Mempool[C]) Compare(ctx sdk.Context, this sdk.Tx, other sdk.Tx) int {
firstPriority := cm.txPriority.GetTxPriority(ctx, this)
secondPriority := cm.txPriority.GetTxPriority(ctx, other)
return cm.txPriority.Compare(firstPriority, secondPriority)
}
+491
View File
@@ -0,0 +1,491 @@
package base
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
// NOTE: THIS IS A COPY OF THE PRIORITY NONCE MEMPOOL FROM COSMOS-SDK. IT HAS BEEN
// MODIFIED FOR OUR USE CASE. THIS CODE WILL BE DEPRECATED ONCE THE COSMOS-SDK
// CUTS A FINAL v0.50.0 RELEASE.
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
// ------------------------------------------------------------------------------ //
import (
"context"
"fmt"
"math"
"github.com/huandu/skiplist"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/cosmos/cosmos-sdk/x/auth/signing"
)
var (
_ sdkmempool.Mempool = (*PriorityNonceMempool[int64])(nil)
_ sdkmempool.Iterator = (*PriorityNonceIterator[int64])(nil)
)
type (
// PriorityNonceMempoolConfig defines the configuration used to configure the
// PriorityNonceMempool.
PriorityNonceMempoolConfig[C comparable] struct {
// TxPriority defines the transaction priority and comparator.
TxPriority TxPriority[C]
// OnRead is a callback to be called when a tx is read from the mempool.
OnRead func(tx sdk.Tx)
// TxReplacement is a callback to be called when duplicated transaction nonce
// detected during mempool insert. An application can define a transaction
// replacement rule based on tx priority or certain transaction fields.
TxReplacement func(op, np C, oTx, nTx sdk.Tx) bool
// MaxTx sets the maximum number of transactions allowed in the mempool with
// the semantics:
// - if MaxTx == 0, there is no cap on the number of transactions in the mempool
// - if MaxTx > 0, the mempool will cap the number of transactions it stores,
// and will prioritize transactions by their priority and sender-nonce
// (sequence number) when evicting transactions.
// - if MaxTx < 0, `Insert` is a no-op.
MaxTx int
}
// PriorityNonceMempool is a mempool implementation that stores txs
// in a partially ordered set by 2 dimensions: priority, and sender-nonce
// (sequence number). Internally it uses one priority ordered skip list and one
// skip list per sender ordered by sender-nonce (sequence number). When there
// are multiple txs from the same sender, they are not always comparable by
// priority to other sender txs and must be partially ordered by both sender-nonce
// and priority.
PriorityNonceMempool[C comparable] struct {
priorityIndex *skiplist.SkipList
priorityCounts map[C]int
senderIndices map[string]*skiplist.SkipList
scores map[txMeta[C]]txMeta[C]
cfg PriorityNonceMempoolConfig[C]
}
// PriorityNonceIterator defines an iterator that is used for mempool iteration
// on Select().
PriorityNonceIterator[C comparable] struct {
mempool *PriorityNonceMempool[C]
priorityNode *skiplist.Element
senderCursors map[string]*skiplist.Element
sender string
nextPriority C
}
// TxPriority defines a type that is used to retrieve and compare transaction
// priorities. Priorities must be comparable.
TxPriority[C comparable] struct {
// GetTxPriority returns the priority of the transaction. A priority must be
// comparable via Compare.
GetTxPriority func(ctx context.Context, tx sdk.Tx) C
// CompareTxPriority compares two transaction priorities. The result should be
// 0 if a == b, -1 if a < b, and +1 if a > b.
Compare func(a, b C) int
// MinValue defines the minimum priority value, e.g. MinInt64. This value is
// used when instantiating a new iterator and comparing weights.
MinValue C
}
// txMeta stores transaction metadata used in indices
txMeta[C comparable] struct {
// nonce is the sender's sequence number
nonce uint64
// priority is the transaction's priority
priority C
// sender is the transaction's sender
sender string
// weight is the transaction's weight, used as a tiebreaker for transactions
// with the same priority
weight C
// senderElement is a pointer to the transaction's element in the sender index
senderElement *skiplist.Element
}
)
// NewDefaultTxPriority returns a TxPriority comparator using ctx.Priority as
// the defining transaction priority.
func NewDefaultTxPriority() TxPriority[int64] {
return TxPriority[int64]{
GetTxPriority: func(goCtx context.Context, _ sdk.Tx) int64 {
return sdk.UnwrapSDKContext(goCtx).Priority()
},
Compare: func(a, b int64) int {
return skiplist.Int64.Compare(a, b)
},
MinValue: math.MinInt64,
}
}
func DefaultPriorityNonceMempoolConfig() PriorityNonceMempoolConfig[int64] {
return PriorityNonceMempoolConfig[int64]{
TxPriority: NewDefaultTxPriority(),
}
}
// skiplistComparable is a comparator for txKeys that first compares priority,
// then weight, then sender, then nonce, uniquely identifying a transaction.
//
// Note, skiplistComparable is used as the comparator in the priority index.
func skiplistComparable[C comparable](txPriority TxPriority[C]) skiplist.Comparable {
return skiplist.LessThanFunc(func(a, b any) int {
keyA := a.(txMeta[C])
keyB := b.(txMeta[C])
res := txPriority.Compare(keyA.priority, keyB.priority)
if res != 0 {
return res
}
// Weight is used as a tiebreaker for transactions with the same priority.
// Weight is calculated in a single pass in .Select(...) and so will be 0
// on .Insert(...).
res = txPriority.Compare(keyA.weight, keyB.weight)
if res != 0 {
return res
}
// Because weight will be 0 on .Insert(...), we must also compare sender and
// nonce to resolve priority collisions. If we didn't then transactions with
// the same priority would overwrite each other in the priority index.
res = skiplist.String.Compare(keyA.sender, keyB.sender)
if res != 0 {
return res
}
return skiplist.Uint64.Compare(keyA.nonce, keyB.nonce)
})
}
// NewPriorityMempool returns the SDK's default mempool implementation which
// returns txs in a partial order by 2 dimensions; priority, and sender-nonce.
func NewPriorityMempool[C comparable](cfg PriorityNonceMempoolConfig[C]) *PriorityNonceMempool[C] {
mp := &PriorityNonceMempool[C]{
priorityIndex: skiplist.New(skiplistComparable(cfg.TxPriority)),
priorityCounts: make(map[C]int),
senderIndices: make(map[string]*skiplist.SkipList),
scores: make(map[txMeta[C]]txMeta[C]),
cfg: cfg,
}
return mp
}
// DefaultPriorityMempool returns a priorityNonceMempool with no options.
func DefaultPriorityMempool() *PriorityNonceMempool[int64] {
return NewPriorityMempool(DefaultPriorityNonceMempoolConfig())
}
// NextSenderTx returns the next transaction for a given sender by nonce order,
// i.e. the next valid transaction for the sender. If no such transaction exists,
// nil will be returned.
func (mp *PriorityNonceMempool[C]) NextSenderTx(sender string) sdk.Tx {
senderIndex, ok := mp.senderIndices[sender]
if !ok {
return nil
}
cursor := senderIndex.Front()
return cursor.Value.(sdk.Tx)
}
// Insert attempts to insert a Tx into the app-side mempool in O(log n) time,
// returning an error if unsuccessful. Sender and nonce are derived from the
// transaction's first signature.
//
// Transactions are unique by sender and nonce. Inserting a duplicate tx is an
// O(log n) no-op.
//
// Inserting a duplicate tx with a different priority overwrites the existing tx,
// changing the total order of the mempool.
func (mp *PriorityNonceMempool[C]) Insert(ctx context.Context, tx sdk.Tx) error {
if mp.cfg.MaxTx > 0 && mp.CountTx() >= mp.cfg.MaxTx {
return sdkmempool.ErrMempoolTxMaxCapacity
} else if mp.cfg.MaxTx < 0 {
return nil
}
sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2()
if err != nil {
return err
}
if len(sigs) == 0 {
return fmt.Errorf("tx must have at least one signer")
}
sig := sigs[0]
sender := sdk.AccAddress(sig.PubKey.Address()).String()
priority := mp.cfg.TxPriority.GetTxPriority(ctx, tx)
nonce := sig.Sequence
key := txMeta[C]{nonce: nonce, priority: priority, sender: sender}
senderIndex, ok := mp.senderIndices[sender]
if !ok {
senderIndex = skiplist.New(skiplist.LessThanFunc(func(a, b any) int {
return skiplist.Uint64.Compare(b.(txMeta[C]).nonce, a.(txMeta[C]).nonce)
}))
// initialize sender index if not found
mp.senderIndices[sender] = senderIndex
}
// Since mp.priorityIndex is scored by priority, then sender, then nonce, a
// changed priority will create a new key, so we must remove the old key and
// re-insert it to avoid having the same tx with different priorityIndex indexed
// twice in the mempool.
//
// This O(log n) remove operation is rare and only happens when a tx's priority
// changes.
sk := txMeta[C]{nonce: nonce, sender: sender}
if oldScore, txExists := mp.scores[sk]; txExists {
if mp.cfg.TxReplacement != nil && !mp.cfg.TxReplacement(oldScore.priority, priority, senderIndex.Get(key).Value.(sdk.Tx), tx) {
return fmt.Errorf(
"tx doesn't fit the replacement rule, oldPriority: %v, newPriority: %v, oldTx: %v, newTx: %v",
oldScore.priority,
priority,
senderIndex.Get(key).Value.(sdk.Tx),
tx,
)
}
mp.priorityIndex.Remove(txMeta[C]{
nonce: nonce,
sender: sender,
priority: oldScore.priority,
weight: oldScore.weight,
})
mp.priorityCounts[oldScore.priority]--
}
mp.priorityCounts[priority]++
// Since senderIndex is scored by nonce, a changed priority will overwrite the
// existing key.
key.senderElement = senderIndex.Set(key, tx)
mp.scores[sk] = txMeta[C]{priority: priority}
mp.priorityIndex.Set(key, tx)
return nil
}
func (i *PriorityNonceIterator[C]) iteratePriority() sdkmempool.Iterator {
// beginning of priority iteration
if i.priorityNode == nil {
i.priorityNode = i.mempool.priorityIndex.Front()
} else {
i.priorityNode = i.priorityNode.Next()
}
// end of priority iteration
if i.priorityNode == nil {
return nil
}
i.sender = i.priorityNode.Key().(txMeta[C]).sender
nextPriorityNode := i.priorityNode.Next()
if nextPriorityNode != nil {
i.nextPriority = nextPriorityNode.Key().(txMeta[C]).priority
} else {
i.nextPriority = i.mempool.cfg.TxPriority.MinValue
}
return i.Next()
}
func (i *PriorityNonceIterator[C]) Next() sdkmempool.Iterator {
if i.priorityNode == nil {
return nil
}
cursor, ok := i.senderCursors[i.sender]
if !ok {
// beginning of sender iteration
cursor = i.mempool.senderIndices[i.sender].Front()
} else {
// middle of sender iteration
cursor = cursor.Next()
}
// end of sender iteration
if cursor == nil {
return i.iteratePriority()
}
key := cursor.Key().(txMeta[C])
// We've reached a transaction with a priority lower than the next highest
// priority in the pool.
if i.priorityNode.Next() != nil {
if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) < 0 {
return i.iteratePriority()
} else if i.mempool.cfg.TxPriority.Compare(key.priority, i.nextPriority) == 0 {
// Weight is incorporated into the priority index key only (not sender index)
// so we must fetch it here from the scores map.
weight := i.mempool.scores[txMeta[C]{nonce: key.nonce, sender: key.sender}].weight
if i.mempool.cfg.TxPriority.Compare(weight, i.priorityNode.Next().Key().(txMeta[C]).weight) < 0 {
return i.iteratePriority()
}
}
}
i.senderCursors[i.sender] = cursor
return i
}
func (i *PriorityNonceIterator[C]) Tx() sdk.Tx {
return i.senderCursors[i.sender].Value.(sdk.Tx)
}
// Select returns a set of transactions from the mempool, ordered by priority
// and sender-nonce in O(n) time. The passed in list of transactions are ignored.
// This is a readonly operation, the mempool is not modified.
//
// The maxBytes parameter defines the maximum number of bytes of transactions to
// return.
func (mp *PriorityNonceMempool[C]) Select(_ context.Context, _ [][]byte) sdkmempool.Iterator {
if mp.priorityIndex.Len() == 0 {
return nil
}
mp.reorderPriorityTies()
iterator := &PriorityNonceIterator[C]{
mempool: mp,
senderCursors: make(map[string]*skiplist.Element),
}
return iterator.iteratePriority()
}
type reorderKey[C comparable] struct {
deleteKey txMeta[C]
insertKey txMeta[C]
tx sdk.Tx
}
func (mp *PriorityNonceMempool[C]) reorderPriorityTies() {
node := mp.priorityIndex.Front()
var reordering []reorderKey[C]
for node != nil {
key := node.Key().(txMeta[C])
if mp.priorityCounts[key.priority] > 1 {
newKey := key
newKey.weight = senderWeight(mp.cfg.TxPriority, key.senderElement)
reordering = append(reordering, reorderKey[C]{deleteKey: key, insertKey: newKey, tx: node.Value.(sdk.Tx)})
}
node = node.Next()
}
for _, k := range reordering {
mp.priorityIndex.Remove(k.deleteKey)
delete(mp.scores, txMeta[C]{nonce: k.deleteKey.nonce, sender: k.deleteKey.sender})
mp.priorityIndex.Set(k.insertKey, k.tx)
mp.scores[txMeta[C]{nonce: k.insertKey.nonce, sender: k.insertKey.sender}] = k.insertKey
}
}
// senderWeight returns the weight of a given tx (t) at senderCursor. Weight is
// defined as the first (nonce-wise) same sender tx with a priority not equal to
// t. It is used to resolve priority collisions, that is when 2 or more txs from
// different senders have the same priority.
func senderWeight[C comparable](txPriority TxPriority[C], senderCursor *skiplist.Element) C {
if senderCursor == nil {
return txPriority.MinValue
}
weight := senderCursor.Key().(txMeta[C]).priority
senderCursor = senderCursor.Next()
for senderCursor != nil {
p := senderCursor.Key().(txMeta[C]).priority
if txPriority.Compare(p, weight) != 0 {
weight = p
}
senderCursor = senderCursor.Next()
}
return weight
}
// CountTx returns the number of transactions in the mempool.
func (mp *PriorityNonceMempool[C]) CountTx() int {
return mp.priorityIndex.Len()
}
// Remove removes a transaction from the mempool in O(log n) time, returning an
// error if unsuccessful.
func (mp *PriorityNonceMempool[C]) Remove(tx sdk.Tx) error {
sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2()
if err != nil {
return err
}
if len(sigs) == 0 {
return fmt.Errorf("attempted to remove a tx with no signatures")
}
sig := sigs[0]
sender := sdk.AccAddress(sig.PubKey.Address()).String()
nonce := sig.Sequence
scoreKey := txMeta[C]{nonce: nonce, sender: sender}
score, ok := mp.scores[scoreKey]
if !ok {
return sdkmempool.ErrTxNotFound
}
tk := txMeta[C]{nonce: nonce, priority: score.priority, sender: sender, weight: score.weight}
senderTxs, ok := mp.senderIndices[sender]
if !ok {
return fmt.Errorf("sender %s not found", sender)
}
mp.priorityIndex.Remove(tk)
senderTxs.Remove(tk)
delete(mp.scores, scoreKey)
mp.priorityCounts[score.priority]--
return nil
}
func IsEmpty[C comparable](mempool sdkmempool.Mempool) error {
mp := mempool.(*PriorityNonceMempool[C])
if mp.priorityIndex.Len() != 0 {
return fmt.Errorf("priorityIndex not empty")
}
countKeys := make([]C, 0, len(mp.priorityCounts))
for k := range mp.priorityCounts {
countKeys = append(countKeys, k)
}
for _, k := range countKeys {
if mp.priorityCounts[k] != 0 {
return fmt.Errorf("priorityCounts not zero at %v, got %v", k, mp.priorityCounts[k])
}
}
senderKeys := make([]string, 0, len(mp.senderIndices))
for k := range mp.senderIndices {
senderKeys = append(senderKeys, k)
}
for _, k := range senderKeys {
if mp.senderIndices[k].Len() != 0 {
return fmt.Errorf("senderIndex not empty for sender %v", k)
}
}
return nil
}