feat: Greedy Algorithm for Lane Verification (#236)
* greedy approach to lane verification * docs * base lane testing * mev lane testing nits * abci top level testing done * network spamming in E2E * removing logs from testing * nit
This commit is contained in:
+18
-19
@@ -76,42 +76,39 @@ func (l *BaseLane) PrepareLane(
|
||||
func (l *BaseLane) ProcessLane(
|
||||
ctx sdk.Context,
|
||||
proposal proposals.Proposal,
|
||||
txs [][]byte,
|
||||
txs []sdk.Tx,
|
||||
next block.ProcessLanesHandler,
|
||||
) (proposals.Proposal, error) {
|
||||
l.Logger().Info("processing lane", "lane", l.Name(), "num_txs_to_verify", len(txs))
|
||||
l.Logger().Info(
|
||||
"processing lane",
|
||||
"lane", l.Name(),
|
||||
"num_txs_to_verify", len(txs),
|
||||
)
|
||||
|
||||
// Assume that this lane is processing sdk.Tx's and decode the transactions.
|
||||
decodedTxs, err := utils.GetDecodedTxs(l.TxDecoder(), txs)
|
||||
if err != nil {
|
||||
l.Logger().Error(
|
||||
"failed to decode transactions",
|
||||
"lane", l.Name(),
|
||||
"err", err,
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
if len(txs) == 0 {
|
||||
return next(ctx, proposal, txs)
|
||||
}
|
||||
|
||||
// Verify the transactions that belong to this lane according to the verification logic of the lane.
|
||||
if err := l.processLaneHandler(ctx, decodedTxs); err != nil {
|
||||
// Verify the transactions that belong to the lane and return any transactions that must be
|
||||
// validated by the next lane in the chain.
|
||||
txsFromLane, remainingTxs, err := l.processLaneHandler(ctx, txs)
|
||||
if err != nil {
|
||||
l.Logger().Error(
|
||||
"failed to process lane",
|
||||
"lane", l.Name(),
|
||||
"err", err,
|
||||
"num_txs_to_verify", len(decodedTxs),
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
// Optimistically update the proposal with the partial proposal.
|
||||
if err := proposal.UpdateProposal(l, decodedTxs); err != nil {
|
||||
if err := proposal.UpdateProposal(l, txsFromLane); err != nil {
|
||||
l.Logger().Error(
|
||||
"failed to update proposal",
|
||||
"lane", l.Name(),
|
||||
"num_txs_verified", len(txsFromLane),
|
||||
"err", err,
|
||||
"num_txs_to_verify", len(decodedTxs),
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
@@ -120,10 +117,12 @@ func (l *BaseLane) ProcessLane(
|
||||
l.Logger().Info(
|
||||
"lane processed",
|
||||
"lane", l.Name(),
|
||||
"num_txs_verified", len(decodedTxs),
|
||||
"num_txs_verified", len(txsFromLane),
|
||||
"num_txs_remaining", len(remainingTxs),
|
||||
)
|
||||
|
||||
return next(ctx, proposal)
|
||||
// Validate the remaining transactions with the next lane in the chain.
|
||||
return next(ctx, proposal, remainingTxs)
|
||||
}
|
||||
|
||||
// VerifyTx verifies that the transaction is valid respecting the ante verification logic of
|
||||
|
||||
+34
-10
@@ -111,35 +111,59 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
|
||||
// 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.
|
||||
// 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 (l *BaseLane) DefaultProcessLaneHandler() ProcessLaneHandler {
|
||||
return func(ctx sdk.Context, partialProposal []sdk.Tx) error {
|
||||
// Process all transactions that match the lane's matcher.
|
||||
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 !l.Match(ctx, tx) {
|
||||
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
|
||||
// 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 := l.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 := l.Compare(ctx, partialProposal[index-1], tx); v == -1 || err != nil {
|
||||
return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
|
||||
return nil, nil, fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := l.VerifyTx(ctx, tx, false); err != nil {
|
||||
return fmt.Errorf("failed to verify tx: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to verify tx: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This means we have processed all transactions in the partial proposal.
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyNoMatches returns an error if any of the transactions match the lane.
|
||||
func (l *BaseLane) VerifyNoMatches(ctx sdk.Context, txs []sdk.Tx) error {
|
||||
for _, tx := range txs {
|
||||
if l.Match(ctx, tx) {
|
||||
return fmt.Errorf("transaction belongs to lane when it should not")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultMatchHandler returns a default implementation of the MatchHandler. It matches all
|
||||
// transactions.
|
||||
func DefaultMatchHandler() MatchHandler {
|
||||
|
||||
+10
-6
@@ -21,9 +21,13 @@ type (
|
||||
) (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. 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
|
||||
// belong to a given lane. The handler must return the transactions that were successfully processed
|
||||
// and the transactions that it cannot process because they belong to a different lane.
|
||||
ProcessLaneHandler func(ctx sdk.Context, partialProposal []sdk.Tx) (
|
||||
txsFromLane []sdk.Tx,
|
||||
remainingTxs []sdk.Tx,
|
||||
err error,
|
||||
)
|
||||
)
|
||||
|
||||
// NoOpPrepareLaneHandler returns a no-op prepare lane handler.
|
||||
@@ -45,15 +49,15 @@ func PanicPrepareLaneHandler() PrepareLaneHandler {
|
||||
// NoOpProcessLaneHandler returns a no-op process lane handler.
|
||||
// This should only be used for testing.
|
||||
func NoOpProcessLaneHandler() ProcessLaneHandler {
|
||||
return func(sdk.Context, []sdk.Tx) error {
|
||||
return nil
|
||||
return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, []sdk.Tx, error) {
|
||||
return nil, nil, 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) error {
|
||||
return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, []sdk.Tx, error) {
|
||||
panic("panic process lanes handler")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ type Lane interface {
|
||||
ProcessLane(
|
||||
ctx sdk.Context,
|
||||
proposal proposals.Proposal,
|
||||
partialProposal [][]byte,
|
||||
txs []sdk.Tx,
|
||||
next ProcessLanesHandler,
|
||||
) (proposals.Proposal, error)
|
||||
|
||||
|
||||
+9
-9
@@ -171,23 +171,23 @@ func (_m *Lane) PrepareLane(ctx types.Context, proposal proposals.Proposal, next
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ProcessLane provides a mock function with given fields: ctx, proposal, partialProposal, next
|
||||
func (_m *Lane) ProcessLane(ctx types.Context, proposal proposals.Proposal, partialProposal [][]byte, next block.ProcessLanesHandler) (proposals.Proposal, error) {
|
||||
ret := _m.Called(ctx, proposal, partialProposal, next)
|
||||
// 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)
|
||||
|
||||
var r0 proposals.Proposal
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) (proposals.Proposal, error)); ok {
|
||||
return rf(ctx, proposal, partialProposal, next)
|
||||
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, []types.Tx, block.ProcessLanesHandler) (proposals.Proposal, error)); ok {
|
||||
return rf(ctx, proposal, txs, next)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) proposals.Proposal); ok {
|
||||
r0 = rf(ctx, proposal, partialProposal, next)
|
||||
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, []types.Tx, block.ProcessLanesHandler) proposals.Proposal); ok {
|
||||
r0 = rf(ctx, proposal, txs, next)
|
||||
} else {
|
||||
r0 = ret.Get(0).(proposals.Proposal)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) error); ok {
|
||||
r1 = rf(ctx, proposal, partialProposal, next)
|
||||
if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, []types.Tx, block.ProcessLanesHandler) error); ok {
|
||||
r1 = rf(ctx, proposal, txs, next)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ func NewProposal(logger log.Logger, txEncoder sdk.TxEncoder, maxBlockSize int64,
|
||||
|
||||
// GetProposalWithInfo returns all of the transactions in the proposal along with information
|
||||
// about the lanes that built the proposal.
|
||||
//
|
||||
// NOTE: This is currently not used in production but likely will be once
|
||||
// ABCI 3.0 is released.
|
||||
func (p *Proposal) GetProposalWithInfo() ([][]byte, error) {
|
||||
// Marshall the proposal info into the first slot of the proposal.
|
||||
infoBz, err := p.Info.Marshal()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package proposals
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
@@ -48,15 +47,13 @@ func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error {
|
||||
return fmt.Errorf("err retrieving transaction info: %s", err)
|
||||
}
|
||||
|
||||
p.Logger.Debug(
|
||||
p.Logger.Info(
|
||||
"updating proposal with tx",
|
||||
"index", index,
|
||||
"lane", lane.Name(),
|
||||
"tx_hash", txInfo.Hash,
|
||||
"tx_size", txInfo.Size,
|
||||
"tx_gas_limit", txInfo.GasLimit,
|
||||
"tx_bytes", txInfo.TxBytes,
|
||||
"raw_tx", base64.StdEncoding.EncodeToString(txInfo.TxBytes),
|
||||
)
|
||||
|
||||
// invariant check: Ensure that the transaction is not already in the proposal.
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ type (
|
||||
// ProcessLanesHandler wraps all of the lanes' ProcessLane functions into a single chained
|
||||
// function. You can think of it like an AnteHandler, but for processing proposals in the
|
||||
// context of lanes instead of modules.
|
||||
ProcessLanesHandler func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error)
|
||||
ProcessLanesHandler func(ctx sdk.Context, proposal proposals.Proposal, txs []sdk.Tx) (proposals.Proposal, error)
|
||||
)
|
||||
|
||||
// NoOpPrepareLanesHandler returns a no-op prepare lanes handler.
|
||||
@@ -29,7 +29,7 @@ func NoOpPrepareLanesHandler() PrepareLanesHandler {
|
||||
// NoOpProcessLanesHandler returns a no-op process lanes handler.
|
||||
// This should only be used for testing.
|
||||
func NoOpProcessLanesHandler() ProcessLanesHandler {
|
||||
return func(_ sdk.Context, p proposals.Proposal) (proposals.Proposal, error) {
|
||||
return func(_ sdk.Context, p proposals.Proposal, _ []sdk.Tx) (proposals.Proposal, error) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
comettypes "github.com/cometbft/cometbft/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
|
||||
@@ -31,8 +33,7 @@ func GetTxInfo(txEncoder sdk.TxEncoder, tx sdk.Tx) (TxInfo, error) {
|
||||
return TxInfo{}, fmt.Errorf("failed to encode transaction: %w", err)
|
||||
}
|
||||
|
||||
txHash := sha256.Sum256(txBz)
|
||||
txHashStr := hex.EncodeToString(txHash[:])
|
||||
txHashStr := strings.ToUpper(hex.EncodeToString(comettypes.Tx(txBz).Hash()))
|
||||
|
||||
// TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc.
|
||||
gasTx, ok := tx.(sdk.FeeTx)
|
||||
|
||||
Reference in New Issue
Block a user