feat(ABCI): New Proposal Struct with Associated Metadata (#126)

* new proto types for proposal info

* new proposal type

* nits

* lane input

* lint

* feat(ABCI): Deprecating `CheckOrderHandler` with new Proposal MetaData (#127)

* refactor without checkorder

* nits

* more nits

* lint

* nits

* feat(ABCI): Updating MEV lane to have no `CheckOrder` handler + testing (#128)

* updating mev lane

* nits

* preventing adding multiple bid txs in prepare

* update
This commit is contained in:
David Terpay
2023-09-28 11:10:13 -04:00
committed by GitHub
parent 3abfde4f34
commit b9d6761776
38 changed files with 3702 additions and 1096 deletions
+68 -20
View File
@@ -4,20 +4,24 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block"
"github.com/skip-mev/block-sdk/block/proposals"
"github.com/skip-mev/block-sdk/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.
// error. The proposal will only be modified if it passes all of the invariant checks.
func (l *BaseLane) PrepareLane(
ctx sdk.Context,
proposal block.BlockProposal,
maxTxBytes int64,
proposal proposals.Proposal,
next block.PrepareLanesHandler,
) (block.BlockProposal, error) {
txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes)
) (proposals.Proposal, error) {
limit := proposal.GetLaneLimits(l.cfg.MaxBlockSpace)
// Select transactions from the lane respecting the selection logic of the lane and the
// max block space for the lane.
txsToInclude, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, limit)
if err != nil {
return proposal, err
}
@@ -31,31 +35,75 @@ func (l *BaseLane) PrepareLane(
)
}
// Update the proposal with the selected transactions.
if err := proposal.UpdateProposal(l, txs); err != nil {
// 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 {
l.Logger().Error(
"failed to update proposal",
"lane", l.Name(),
"err", err,
"num_txs_to_add", len(txsToInclude),
"num_txs_to_remove", len(txsToRemove),
)
return proposal, err
}
l.Logger().Info(
"lane prepared",
"lane", l.Name(),
"num_txs_added", len(txsToInclude),
"num_txs_removed", len(txsToRemove),
)
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)
// the verification logic of the lane (processLaneHandler). If any of the transactions are invalid,
// we return an error. If all of the transactions are valid, we return the updated proposal.
func (l *BaseLane) ProcessLane(
ctx sdk.Context,
proposal proposals.Proposal,
txs [][]byte,
next block.ProcessLanesHandler,
) (proposals.Proposal, error) {
// Assume that this lane is processing sdk.Tx's and decode the transactions.
decodedTxs, err := utils.GetDecodedTxs(l.TxDecoder(), txs)
if err != nil {
return ctx, err
l.Logger().Error(
"failed to decode transactions",
"lane", l.Name(),
"err", err,
)
return proposal, err
}
return next(ctx, remainingTxs)
// Verify the transactions that belong to this lane according to the verification logic of the lane.
if err := l.processLaneHandler(ctx, decodedTxs); err != nil {
return proposal, err
}
// Optimistically update the proposal with the partial proposal.
if err := proposal.UpdateProposal(l, decodedTxs); err != nil {
l.Logger().Error(
"failed to update proposal",
"lane", l.Name(),
"err", err,
"num_txs_to_verify", len(decodedTxs),
)
return proposal, err
}
l.Logger().Info(
"lane processed",
"lane", l.Name(),
"num_txs_verified", len(decodedTxs),
)
return next(ctx, proposal)
}
// AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of
+55 -65
View File
@@ -5,20 +5,21 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block"
"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
// 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
// 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 (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) {
return func(ctx sdk.Context, proposal proposals.Proposal, limit proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
var (
totalSize int64
txs [][]byte
txsToRemove []sdk.Tx
totalSize int64
totalGas uint64
txsToInclude []sdk.Tx
txsToRemove []sdk.Tx
)
// Select all transactions in the mempool that are valid and not already in the
@@ -26,7 +27,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
tx := iterator.Tx()
txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx)
txInfo, err := utils.GetTxInfo(l.TxEncoder(), tx)
if err != nil {
l.Logger().Info("failed to get hash of tx", "err", err)
@@ -38,7 +39,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
if !l.Match(ctx, tx) {
l.Logger().Info(
"failed to select tx for lane; tx does not belong to lane",
"tx_hash", hash,
"tx_hash", txInfo.Hash,
"lane", l.Name(),
)
@@ -47,10 +48,10 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
}
// if the transaction is already in the (partial) block proposal, we skip it.
if proposal.Contains(txBytes) {
if proposal.Contains(txInfo.Hash) {
l.Logger().Info(
"failed to select tx for lane; tx is already in proposal",
"tx_hash", hash,
"tx_hash", txInfo.Hash,
"lane", l.Name(),
)
@@ -58,25 +59,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
}
// 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 {
if updatedSize := totalSize + txInfo.Size; updatedSize > limit.MaxTxBytes {
l.Logger().Info(
"tx bytes above the maximum allowed",
"failed to select tx for lane; tx bytes above the maximum allowed",
"lane", l.Name(),
"tx_size", txSize,
"tx_size", txInfo.Size,
"total_size", totalSize,
"max_tx_bytes", maxTxBytes,
"tx_hash", hash,
"max_tx_bytes", limit.MaxTxBytes,
"tx_hash", txInfo.Hash,
)
break
// 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 {
l.Logger().Info(
"failed to select tx for lane; gas limit above the maximum allowed",
"lane", l.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 ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil {
l.Logger().Info(
"failed to verify tx",
"tx_hash", hash,
"tx_hash", txInfo.Hash,
"err", err,
)
@@ -84,66 +100,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
continue
}
totalSize += txSize
txs = append(txs, txBytes)
totalSize += txInfo.Size
totalGas += txInfo.GasLimit
txsToInclude = append(txsToInclude, tx)
}
return txs, txsToRemove, nil
return txsToInclude, 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.
// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It verifies
// the following invariants:
// 1. All transactions belong to this lane.
// 2. All transactions respect the priority defined by the mempool.
// 3. All transactions are valid respecting the verification logic of the lane.
func (l *BaseLane) DefaultProcessLaneHandler() ProcessLaneHandler {
return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) {
var err error
return func(ctx sdk.Context, partialProposal []sdk.Tx) 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
for index, tx := range partialProposal {
if !l.Match(ctx, tx) {
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
}
}
// 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() 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, partialProposal[index-1], tx) == -1 {
return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
}
// 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
if _, err := l.AnteVerifyTx(ctx, tx, false); err != nil {
return fmt.Errorf("failed to verify tx: %w", err)
}
}
// This means we have processed all transactions in the partial proposal.
return nil
}
}
-21
View File
@@ -38,11 +38,6 @@ type BaseLane struct { //nolint
// requested and the lane needs to submit transactions it wants included in the block.
prepareLaneHandler 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 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.
@@ -95,10 +90,6 @@ func (l *BaseLane) ValidateBasic() error {
l.processLaneHandler = l.DefaultProcessLaneHandler()
}
if l.checkOrderHandler == nil {
l.checkOrderHandler = l.DefaultCheckOrderHandler()
}
return nil
}
@@ -125,18 +116,6 @@ func (l *BaseLane) SetProcessLaneHandler(processLaneHandler ProcessLaneHandler)
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 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
+6 -6
View File
@@ -103,13 +103,13 @@ func (cm *Mempool[C]) Insert(ctx context.Context, tx sdk.Tx) error {
return fmt.Errorf("failed to insert tx into auction index: %w", err)
}
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
if err != nil {
cm.Remove(tx)
return err
}
cm.txCache[txHashStr] = struct{}{}
cm.txCache[txInfo.Hash] = struct{}{}
return nil
}
@@ -120,12 +120,12 @@ func (cm *Mempool[C]) Remove(tx sdk.Tx) error {
return fmt.Errorf("failed to remove transaction from the mempool: %w", err)
}
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
if err != nil {
return fmt.Errorf("failed to get tx hash string: %w", err)
}
delete(cm.txCache, txHashStr)
delete(cm.txCache, txInfo.Hash)
return nil
}
@@ -145,12 +145,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 {
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
if err != nil {
return false
}
_, ok := cm.txCache[txHashStr]
_, ok := cm.txCache[txInfo.Hash]
return ok
}
+12 -20
View File
@@ -3,7 +3,7 @@ package base
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block"
"github.com/skip-mev/block-sdk/block/proposals"
)
type (
@@ -16,28 +16,20 @@ type (
// the transactions that must be removed from the lane, and an error if one occurred.
PrepareLaneHandler func(
ctx sdk.Context,
proposal block.BlockProposal,
maxTxBytes int64,
) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error)
proposal proposals.Proposal,
limit proposals.LaneLimits,
) (txsToInclude []sdk.Tx, txsToRemove []sdk.Tx, err error)
// 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)
// 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
// belong to a given lane. This handler must return an error if the transactions are not correctly
// ordered, do not belong to this lane, or any other relevant error.
ProcessLaneHandler func(ctx sdk.Context, partialProposal []sdk.Tx) error
)
// NoOpPrepareLaneHandler returns a no-op prepare lane handler.
// This should only be used for testing.
func NoOpPrepareLaneHandler() PrepareLaneHandler {
return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) {
return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
return nil, nil, nil
}
}
@@ -45,7 +37,7 @@ func NoOpPrepareLaneHandler() PrepareLaneHandler {
// PanicPrepareLaneHandler returns a prepare lane handler that panics.
// This should only be used for testing.
func PanicPrepareLaneHandler() PrepareLaneHandler {
return func(sdk.Context, block.BlockProposal, int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) {
return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
panic("panic prepare lanes handler")
}
}
@@ -53,15 +45,15 @@ func PanicPrepareLaneHandler() PrepareLaneHandler {
// NoOpProcessLaneHandler returns a no-op process lane handler.
// This should only be used for testing.
func NoOpProcessLaneHandler() ProcessLaneHandler {
return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) {
return txs, nil
return func(sdk.Context, []sdk.Tx) error {
return nil
}
}
// PanicProcessLanesHandler returns a process lanes handler that panics.
// This should only be used for testing.
func PanicProcessLaneHandler() ProcessLaneHandler {
return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, error) {
return func(sdk.Context, []sdk.Tx) error {
panic("panic process lanes handler")
}
}