block-sdk/block/base/proposals.go
2024-06-25 11:58:04 -04:00

193 lines
6.1 KiB
Go

package base
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/v2/block/proposals"
)
// DefaultProposalHandler returns a default implementation of the PrepareLaneHandler and
// ProcessLaneHandler.
type DefaultProposalHandler struct {
lane *BaseLane
}
// NewDefaultProposalHandler returns a new default proposal handler.
func NewDefaultProposalHandler(lane *BaseLane) *DefaultProposalHandler {
return &DefaultProposalHandler{
lane: lane,
}
}
// 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 blockspace/gas for this
// lane has been reached. Additionally, any transactions that are invalid will be returned.
func (h *DefaultProposalHandler) PrepareLaneHandler() PrepareLaneHandler {
return func(ctx sdk.Context, proposal proposals.Proposal, limit proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
var (
totalSize int64
totalGas uint64
txsToInclude []sdk.Tx
txsToRemove []sdk.Tx
)
// Select all transactions in the mempool that are valid and not already in the
// partial proposal.
for iterator := h.lane.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
tx := iterator.Tx()
txInfo, err := h.lane.GetTxInfo(ctx, tx)
if err != nil {
h.lane.Logger().Info("failed to get hash of tx", "err", err)
txsToRemove = append(txsToRemove, tx)
continue
}
if txInfo.GasLimit > limit.MaxGasLimit {
h.lane.Logger().Info(
"failed to select tx for lane; gas limit above the maximum allowed",
"lane", h.lane.Name(),
"tx_gas", txInfo.GasLimit,
"max_gas", limit.MaxGasLimit,
"tx_hash", txInfo.Hash,
)
txsToRemove = append(txsToRemove, tx)
continue
}
if txInfo.Size > limit.MaxTxBytes {
h.lane.Logger().Info(
"failed to select tx for lane; tx bytes above the maximum allowed",
"lane", h.lane.Name(),
"tx_size", txInfo.Size,
"max_tx_bytes", limit.MaxTxBytes,
"tx_hash", txInfo.Hash,
)
txsToRemove = append(txsToRemove, tx)
continue
}
// Double check that the transaction belongs to this lane.
if !h.lane.Match(ctx, tx) {
h.lane.Logger().Info(
"failed to select tx for lane; tx does not belong to lane",
"tx_hash", txInfo.Hash,
"lane", h.lane.Name(),
)
txsToRemove = append(txsToRemove, tx)
continue
}
// if the transaction is already in the (partial) block proposal, we skip it.
if proposal.Contains(txInfo.Hash) {
h.lane.Logger().Info(
"failed to select tx for lane; tx is already in proposal",
"tx_hash", txInfo.Hash,
"lane", h.lane.Name(),
)
continue
}
// If the transaction is too large, we break and do not attempt to include more txs.
if updatedSize := totalSize + txInfo.Size; updatedSize > limit.MaxTxBytes {
h.lane.Logger().Info(
"failed to select tx for lane; tx bytes above the maximum allowed",
"lane", h.lane.Name(),
"tx_size", txInfo.Size,
"total_size", totalSize,
"max_tx_bytes", limit.MaxTxBytes,
"tx_hash", txInfo.Hash,
)
// TODO: Determine if there is any trade off with breaking or continuing here.
continue
}
// If the gas limit of the transaction is too large, we break and do not attempt to include more txs.
if updatedGas := totalGas + txInfo.GasLimit; updatedGas > limit.MaxGasLimit {
h.lane.Logger().Info(
"failed to select tx for lane; gas limit above the maximum allowed",
"lane", h.lane.Name(),
"tx_gas", txInfo.GasLimit,
"total_gas", totalGas,
"max_gas", limit.MaxGasLimit,
"tx_hash", txInfo.Hash,
)
// TODO: Determine if there is any trade off with breaking or continuing here.
continue
}
// Verify the transaction.
if err = h.lane.VerifyTx(ctx, tx, false); err != nil {
h.lane.Logger().Info(
"failed to verify tx",
"tx_hash", txInfo.Hash,
"err", err,
)
txsToRemove = append(txsToRemove, tx)
continue
}
totalSize += txInfo.Size
totalGas += txInfo.GasLimit
txsToInclude = append(txsToInclude, tx)
}
return txsToInclude, txsToRemove, nil
}
}
// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It verifies
// the following invariants:
// 1. Transactions belonging to the lane must be contiguous from the beginning of the partial proposal.
// 2. Transactions that do not belong to the lane must be contiguous from the end of the partial proposal.
// 3. Transactions must be ordered respecting the priority defined by the lane (e.g. gas price).
// 4. Transactions must be valid according to the verification logic of the lane.
func (h *DefaultProposalHandler) ProcessLaneHandler() ProcessLaneHandler {
return func(ctx sdk.Context, partialProposal []sdk.Tx) ([]sdk.Tx, []sdk.Tx, error) {
if len(partialProposal) == 0 {
return nil, nil, nil
}
for index, tx := range partialProposal {
if !h.lane.Match(ctx, tx) {
// If the transaction does not belong to this lane, we return the remaining transactions
// iff there are no matches in the remaining transactions after this index.
if index+1 < len(partialProposal) {
if err := h.lane.VerifyNoMatches(ctx, partialProposal[index+1:]); err != nil {
return nil, nil, fmt.Errorf("failed to verify no matches: %w", err)
}
}
return partialProposal[:index], partialProposal[index:], nil
}
// If the transactions do not respect the priority defined by the mempool, we consider the proposal
// to be invalid
if index > 0 {
if v, err := h.lane.Compare(ctx, partialProposal[index-1], tx); v == -1 || err != nil {
return nil, nil, fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
}
}
if err := h.lane.VerifyTx(ctx, tx, false); err != nil {
return nil, nil, fmt.Errorf("failed to verify tx: %w", err)
}
}
// This means we have processed all transactions in the partial proposal i.e.
// all of the transactions belong to this lane. There are no remaining transactions.
return partialProposal, nil, nil
}
}