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 -31
View File
@@ -9,11 +9,7 @@ import (
"github.com/skip-mev/block-sdk/block"
"github.com/skip-mev/block-sdk/block/proposals"
)
const (
// ProposalInfoIndex is the index of the proposal metadata in the proposal.
ProposalInfoIndex = 0
"github.com/skip-mev/block-sdk/block/utils"
)
type (
@@ -78,25 +74,17 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
return &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}
prepareLanesHandler := ChainPrepareLanes(registry)
// Fill the proposal with transactions from each lane.
prepareLanesHandler := ChainPrepareLanes(registry)
finalProposal, err := prepareLanesHandler(ctx, proposals.NewProposalWithContext(h.logger, ctx, h.txEncoder))
if err != nil {
h.logger.Error("failed to prepare proposal", "err", err)
return &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}
// Retrieve the proposal with metadata and transactions.
txs, err := finalProposal.GetProposalWithInfo()
if err != nil {
h.logger.Error("failed to get proposal with metadata", "err", err)
return &abci.ResponsePrepareProposal{Txs: make([][]byte, 0)}, err
}
h.logger.Info(
"prepared proposal",
"num_txs", len(txs),
"num_txs", len(finalProposal.Txs),
"total_tx_bytes", finalProposal.Info.BlockSize,
"max_tx_bytes", finalProposal.Info.MaxBlockSize,
"total_gas_limit", finalProposal.Info.GasLimit,
@@ -111,7 +99,7 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
)
return &abci.ResponsePrepareProposal{
Txs: txs,
Txs: finalProposal.Txs,
}, nil
}
}
@@ -119,9 +107,9 @@ func (h *ProposalHandler) PrepareProposalHandler() sdk.PrepareProposalHandler {
// ProcessProposalHandler processes the proposal by verifying all transactions in the proposal
// according to each lane's verification logic. Proposals are verified similar to how they are
// constructed. After a proposal is processed, it should amount to the same proposal that was prepared.
// Each proposal will first be broken down by the lanes that prepared each partial proposal. Then, each
// lane will iteratively verify the transactions that it belong to it. If any lane fails to verify the
// transactions, then the proposal is rejected.
// The proposal is verified in a greedy fashion, respecting the ordering of lanes. A lane will
// verify all transactions in the proposal that belong to the lane and pass any remaining transactions
// to the next lane in the chain.
func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return func(ctx sdk.Context, req *abci.RequestProcessProposal) (resp *abci.ResponseProcessProposal, err error) {
if req.Height <= 1 {
@@ -138,10 +126,10 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
}
}()
// Extract all of the lanes and their corresponding transactions from the proposal.
proposalInfo, partialProposals, err := h.ExtractLanes(ctx, req.Txs)
// Decode the transactions in the proposal. These will be verified by each lane in a greedy fashion.
decodedTxs, err := utils.GetDecodedTxs(h.txDecoder, req.Txs)
if err != nil {
h.logger.Error("failed to validate proposal", "err", err)
h.logger.Error("failed to decode txs", "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err
}
@@ -152,22 +140,21 @@ func (h *ProposalHandler) ProcessProposalHandler() sdk.ProcessProposalHandler {
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err
}
processLanesHandler := ChainProcessLanes(partialProposals, registry)
finalProposal, err := processLanesHandler(ctx, proposals.NewProposalWithContext(h.logger, ctx, h.txEncoder))
// Verify the proposal.
processLanesHandler := ChainProcessLanes(registry)
finalProposal, err := processLanesHandler(
ctx,
proposals.NewProposalWithContext(h.logger, ctx, h.txEncoder),
decodedTxs,
)
if err != nil {
h.logger.Error("failed to validate the proposal", "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err
}
// Ensure block size and gas limit are correct.
if err := h.ValidateBlockLimits(finalProposal, proposalInfo); err != nil {
h.logger.Error("failed to validate the proposal", "err", err)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, err
}
h.logger.Info(
"processed proposal",
"num_txs", len(req.Txs),
"num_txs", len(finalProposal.Txs),
"total_tx_bytes", finalProposal.Info.BlockSize,
"max_tx_bytes", finalProposal.Info.MaxBlockSize,
"total_gas_limit", finalProposal.Info.GasLimit,
+197 -560
View File
File diff suppressed because it is too large Load Diff
+7 -101
View File
@@ -1,108 +1,13 @@
package abci
import (
"fmt"
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/proposals/types"
"github.com/skip-mev/block-sdk/lanes/terminator"
)
// ExtractLanes validates the proposal against the basic invariants that are required
// for the proposal to be valid. This includes:
// 1. The proposal must contain the proposal information and must be valid.
// 2. The proposal must contain the correct number of transactions for each lane.
func (h *ProposalHandler) ExtractLanes(ctx sdk.Context, proposal [][]byte) (types.ProposalInfo, [][][]byte, error) {
// If the proposal is empty, then the metadata was not included.
if len(proposal) == 0 {
return types.ProposalInfo{}, nil, fmt.Errorf("proposal does not contain proposal metadata")
}
metaDataBz, txs := proposal[ProposalInfoIndex], proposal[ProposalInfoIndex+1:]
// Retrieve the metadata from the proposal.
var metaData types.ProposalInfo
if err := metaData.Unmarshal(metaDataBz); err != nil {
return types.ProposalInfo{}, nil, fmt.Errorf("failed to unmarshal proposal metadata: %w", err)
}
lanes, err := h.mempool.Registry(ctx)
if err != nil {
return types.ProposalInfo{}, nil, fmt.Errorf("failed to get mempool registry: %w", err)
}
partialProposals := make([][][]byte, len(lanes))
if metaData.TxsByLane == nil {
if len(txs) > 0 {
return types.ProposalInfo{}, nil, fmt.Errorf("proposal contains invalid number of transactions")
}
return types.ProposalInfo{}, partialProposals, nil
}
h.logger.Info(
"received proposal with metadata",
"max_block_size", metaData.MaxBlockSize,
"max_gas_limit", metaData.MaxGasLimit,
"gas_limit", metaData.GasLimit,
"block_size", metaData.BlockSize,
"lanes_with_txs", metaData.TxsByLane,
)
// Iterate through all of the lanes and match the corresponding transactions to the lane.
for index, lane := range lanes {
numTxs := metaData.TxsByLane[lane.Name()]
if numTxs > uint64(len(txs)) {
return types.ProposalInfo{}, nil, fmt.Errorf(
"proposal metadata contains invalid number of transactions for lane %s; got %d, expected %d",
lane.Name(),
len(txs),
numTxs,
)
}
partialProposals[index] = txs[:numTxs]
txs = txs[numTxs:]
}
// If there are any transactions remaining in the proposal, then the proposal is invalid.
if len(txs) > 0 {
return types.ProposalInfo{}, nil, fmt.Errorf("proposal contains invalid number of transactions")
}
return metaData, partialProposals, nil
}
// ValidateBlockLimits validates the block limits of the proposal against the block limits
// of the chain.
func (h *ProposalHandler) ValidateBlockLimits(finalProposal proposals.Proposal, proposalInfo types.ProposalInfo) error {
// Conduct final checks on block size and gas limit.
if finalProposal.Info.BlockSize != proposalInfo.BlockSize {
h.logger.Error(
"proposal block size does not match",
"expected", proposalInfo.BlockSize,
"got", finalProposal.Info.BlockSize,
)
return fmt.Errorf("proposal block size does not match")
}
if finalProposal.Info.GasLimit != proposalInfo.GasLimit {
h.logger.Error(
"proposal gas limit does not match",
"expected", proposalInfo.GasLimit,
"got", finalProposal.Info.GasLimit,
)
return fmt.Errorf("proposal gas limit does not match")
}
return nil
}
// ChainPrepareLanes chains together the proposal preparation logic from each lane into a
// single function. The first lane in the chain is the first lane to be prepared and the
// last lane in the chain is the last lane to be prepared. In the case where any of the lanes
@@ -162,8 +67,11 @@ func ChainPrepareLanes(chain []block.Lane) block.PrepareLanesHandler {
// ChainProcessLanes chains together the proposal verification logic from each lane
// into a single function. The first lane in the chain is the first lane to be verified and
// the last lane in the chain is the last lane to be verified. Each lane will validate
// the transactions that it selected in the prepare phase.
func ChainProcessLanes(partialProposals [][][]byte, chain []block.Lane) block.ProcessLanesHandler {
// the transactions that belong to the lane and pass any remaining transactions to the next
// lane in the chain. If any of the lanes fail to verify the transactions, the proposal will
// be rejected. If there are any remaining transactions after all lanes have been processed,
// the proposal will be rejected.
func ChainProcessLanes(chain []block.Lane) block.ProcessLanesHandler {
if len(chain) == 0 {
return nil
}
@@ -171,12 +79,10 @@ func ChainProcessLanes(partialProposals [][][]byte, chain []block.Lane) block.Pr
// Handle non-terminated decorators chain
if (chain[len(chain)-1] != terminator.Terminator{}) {
chain = append(chain, terminator.Terminator{})
partialProposals = append(partialProposals, nil)
}
return func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) {
return func(ctx sdk.Context, proposal proposals.Proposal, txs []sdk.Tx) (proposals.Proposal, error) {
lane := chain[0]
partialProposal := partialProposals[0]
return lane.ProcessLane(ctx, proposal, partialProposal, ChainProcessLanes(partialProposals[1:], chain[1:]))
return lane.ProcessLane(ctx, proposal, txs, ChainProcessLanes(chain[1:]))
}
}
+25 -63
View File
@@ -17,9 +17,6 @@ import (
signeradaptors "github.com/skip-mev/block-sdk/adapters/signer_extraction_adapter"
"github.com/skip-mev/block-sdk/block"
"github.com/skip-mev/block-sdk/block/base"
"github.com/skip-mev/block-sdk/block/proposals"
"github.com/skip-mev/block-sdk/block/proposals/types"
"github.com/skip-mev/block-sdk/block/utils"
defaultlane "github.com/skip-mev/block-sdk/lanes/base"
"github.com/skip-mev/block-sdk/lanes/free"
"github.com/skip-mev/block-sdk/lanes/mev"
@@ -58,6 +55,29 @@ func (s *ProposalsTestSuite) setUpAnteHandler(expectedExecution map[sdk.Tx]bool)
return anteHandler
}
func (s *ProposalsTestSuite) setUpCustomMatchHandlerLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool, mh base.MatchHandler, name string) block.Lane {
cfg := base.LaneConfig{
Logger: log.NewNopLogger(),
TxEncoder: s.encodingConfig.TxConfig.TxEncoder(),
TxDecoder: s.encodingConfig.TxConfig.TxDecoder(),
AnteHandler: s.setUpAnteHandler(expectedExecution),
MaxBlockSpace: maxBlockSpace,
SignerExtractor: signeradaptors.NewDefaultAdapter(),
}
lane := base.NewBaseLane(
cfg,
name,
base.NewMempool[string](base.DefaultTxPriority(), cfg.TxEncoder, cfg.SignerExtractor, 0),
mh,
)
lane.SetPrepareLaneHandler(lane.DefaultPrepareLaneHandler())
lane.SetProcessLaneHandler(lane.DefaultProcessLaneHandler())
return lane
}
func (s *ProposalsTestSuite) setUpStandardLane(maxBlockSpace math.LegacyDec, expectedExecution map[sdk.Tx]bool) *defaultlane.DefaultLane {
cfg := base.LaneConfig{
Logger: log.NewNopLogger(),
@@ -153,51 +173,8 @@ func (s *ProposalsTestSuite) setUpProposalHandlers(lanes []block.Lane) *abci.Pro
)
}
func (s *ProposalsTestSuite) createProposal(distribution map[string]uint64, txs ...sdk.Tx) [][]byte {
maxSize, maxGasLimit := proposals.GetBlockLimits(s.ctx)
size, limit := s.getTxInfos(txs...)
info := s.createProposalInfoBytes(
maxGasLimit,
limit,
maxSize,
size,
distribution,
)
proposal := s.getTxBytes(txs...)
return append([][]byte{info}, proposal...)
}
func (s *ProposalsTestSuite) getProposalInfo(bz []byte) types.ProposalInfo {
var info types.ProposalInfo
s.Require().NoError(info.Unmarshal(bz))
return info
}
func (s *ProposalsTestSuite) createProposalInfo(
maxGasLimit, gasLimit uint64,
maxBlockSize, blockSize int64,
txsByLane map[string]uint64,
) types.ProposalInfo {
return types.ProposalInfo{
MaxGasLimit: maxGasLimit,
GasLimit: gasLimit,
MaxBlockSize: maxBlockSize,
BlockSize: blockSize,
TxsByLane: txsByLane,
}
}
func (s *ProposalsTestSuite) createProposalInfoBytes(
maxGasLimit, gasLimit uint64,
maxBlockSize, blockSize int64,
txsByLane map[string]uint64,
) []byte {
info := s.createProposalInfo(maxGasLimit, gasLimit, maxBlockSize, blockSize, txsByLane)
bz, err := info.Marshal()
s.Require().NoError(err)
return bz
func (s *ProposalsTestSuite) createProposal(txs ...sdk.Tx) [][]byte {
return s.getTxBytes(txs...)
}
func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte {
@@ -211,21 +188,6 @@ func (s *ProposalsTestSuite) getTxBytes(txs ...sdk.Tx) [][]byte {
return txBytes
}
func (s *ProposalsTestSuite) getTxInfos(txs ...sdk.Tx) (int64, uint64) {
totalSize := int64(0)
totalGasLimit := uint64(0)
for _, tx := range txs {
info, err := utils.GetTxInfo(s.encodingConfig.TxConfig.TxEncoder(), tx)
s.Require().NoError(err)
totalSize += info.Size
totalGasLimit += info.GasLimit
}
return totalSize, totalGasLimit
}
func (s *ProposalsTestSuite) setBlockParams(maxGasLimit, maxBlockSize int64) {
s.ctx = s.ctx.WithConsensusParams(
tmprototypes.ConsensusParams{