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
-31
@@ -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
File diff suppressed because it is too large
Load Diff
+7
-101
@@ -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
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user