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:
David Terpay
2023-11-27 16:43:09 -05:00
committed by GitHub
parent f607439637
commit f7dfbda2b1
21 changed files with 1213 additions and 935 deletions
+18 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+3
View File
@@ -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 -4
View File
@@ -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
View File
@@ -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
}
}
+4 -3
View File
@@ -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)