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
+1 -7
View File
@@ -1,16 +1,10 @@
package utils
package block
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
type (
// Lane defines the required API dependencies for the IgnoreDecorator. The ignore decorator
// will check if a transaction belongs to a lane by calling the Match function.
Lane interface {
Match(ctx sdk.Context, tx sdk.Tx) bool
}
// IgnoreDecorator is an AnteDecorator that wraps an existing AnteDecorator. It allows
// for the AnteDecorator to be ignored for specified lanes.
IgnoreDecorator struct {
+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")
}
}
+20 -17
View File
@@ -5,13 +5,14 @@ import (
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
"github.com/skip-mev/block-sdk/block/proposals"
)
// LaneMempool defines the interface a lane's mempool should implement. The basic API
// is the same as the sdk.Mempool, but it also includes a Compare function that is used
// to determine the relative priority of two transactions belonging in the same lane.
//
//go:generate mockery --name LaneMempool --output ./utils/mocks --outpkg mocks --case underscore
//go:generate mockery --name LaneMempool --output ./mocks --outpkg mocks --case underscore
type LaneMempool interface {
sdkmempool.Mempool
@@ -27,29 +28,31 @@ type LaneMempool interface {
// Lane defines an interface used for matching transactions to lanes, storing transactions,
// and constructing partial blocks.
//
//go:generate mockery --name Lane --output ./utils/mocks --outpkg mocks --case underscore
//go:generate mockery --name Lane --output ./mocks --outpkg mocks --case underscore
type Lane interface {
LaneMempool
// PrepareLane builds a portion of the block. It inputs the maxTxBytes that can be
// included in the proposal for the given lane, the partial proposal, and a function
// to call the next lane in the chain. The next lane in the chain will be called with
// the updated proposal and context.
// PrepareLane builds a portion of the block. It inputs the current context, proposal, and a
// function to call the next lane in the chain. This handler should update the context as needed
// and add transactions to the proposal. Note, the lane should only add transactions up to the
// max block space for the lane.
PrepareLane(
ctx sdk.Context,
proposal BlockProposal,
maxTxBytes int64,
proposal proposals.Proposal,
next PrepareLanesHandler,
) (BlockProposal, error)
) (proposals.Proposal, error)
// CheckOrder validates that transactions belonging to this lane are not misplaced
// in the block proposal and respect the ordering rules of the lane.
CheckOrder(ctx sdk.Context, txs []sdk.Tx) error
// ProcessLane verifies this lane's portion of a proposed block. It inputs the transactions
// that may belong to this lane and a function to call the next lane in the chain. The next
// lane in the chain will be called with the updated context and filtered down transactions.
ProcessLane(ctx sdk.Context, proposalTxs []sdk.Tx, next ProcessLanesHandler) (sdk.Context, error)
// ProcessLane verifies this lane's portion of a proposed block. It inputs the current context,
// proposal, transactions that belong to this lane, and a function to call the next lane in the
// chain. This handler should update the context as needed and add transactions to the proposal.
// The entire process lane chain should end up constructing the same proposal as the prepare lane
// chain.
ProcessLane(
ctx sdk.Context,
proposal proposals.Proposal,
partialProposal [][]byte,
next ProcessLanesHandler,
) (proposals.Proposal, error)
// GetMaxBlockSpace returns the max block space for the lane as a relative percentage.
GetMaxBlockSpace() math.LegacyDec
@@ -15,6 +15,8 @@ import (
mock "github.com/stretchr/testify/mock"
proposals "github.com/skip-mev/block-sdk/block/proposals"
types "github.com/cosmos/cosmos-sdk/types"
)
@@ -23,20 +25,6 @@ type Lane struct {
mock.Mock
}
// CheckOrder provides a mock function with given fields: ctx, txs
func (_m *Lane) CheckOrder(ctx types.Context, txs []types.Tx) error {
ret := _m.Called(ctx, txs)
var r0 error
if rf, ok := ret.Get(0).(func(types.Context, []types.Tx) error); ok {
r0 = rf(ctx, txs)
} else {
r0 = ret.Error(0)
}
return r0
}
// Compare provides a mock function with given fields: ctx, this, other
func (_m *Lane) Compare(ctx types.Context, this types.Tx, other types.Tx) int {
ret := _m.Called(ctx, this, other)
@@ -151,25 +139,23 @@ func (_m *Lane) Name() string {
return r0
}
// PrepareLane provides a mock function with given fields: ctx, proposal, maxTxBytes, next
func (_m *Lane) PrepareLane(ctx types.Context, proposal block.BlockProposal, maxTxBytes int64, next block.PrepareLanesHandler) (block.BlockProposal, error) {
ret := _m.Called(ctx, proposal, maxTxBytes, next)
// PrepareLane provides a mock function with given fields: ctx, proposal, next
func (_m *Lane) PrepareLane(ctx types.Context, proposal proposals.Proposal, next block.PrepareLanesHandler) (proposals.Proposal, error) {
ret := _m.Called(ctx, proposal, next)
var r0 block.BlockProposal
var r0 proposals.Proposal
var r1 error
if rf, ok := ret.Get(0).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) (block.BlockProposal, error)); ok {
return rf(ctx, proposal, maxTxBytes, next)
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) (proposals.Proposal, error)); ok {
return rf(ctx, proposal, next)
}
if rf, ok := ret.Get(0).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) block.BlockProposal); ok {
r0 = rf(ctx, proposal, maxTxBytes, next)
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) proposals.Proposal); ok {
r0 = rf(ctx, proposal, next)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(block.BlockProposal)
}
r0 = ret.Get(0).(proposals.Proposal)
}
if rf, ok := ret.Get(1).(func(types.Context, block.BlockProposal, int64, block.PrepareLanesHandler) error); ok {
r1 = rf(ctx, proposal, maxTxBytes, next)
if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, block.PrepareLanesHandler) error); ok {
r1 = rf(ctx, proposal, next)
} else {
r1 = ret.Error(1)
}
@@ -177,23 +163,23 @@ func (_m *Lane) PrepareLane(ctx types.Context, proposal block.BlockProposal, max
return r0, r1
}
// ProcessLane provides a mock function with given fields: ctx, proposalTxs, next
func (_m *Lane) ProcessLane(ctx types.Context, proposalTxs []types.Tx, next block.ProcessLanesHandler) (types.Context, error) {
ret := _m.Called(ctx, proposalTxs, next)
// 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)
var r0 types.Context
var r0 proposals.Proposal
var r1 error
if rf, ok := ret.Get(0).(func(types.Context, []types.Tx, block.ProcessLanesHandler) (types.Context, error)); ok {
return rf(ctx, proposalTxs, next)
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, []types.Tx, block.ProcessLanesHandler) types.Context); ok {
r0 = rf(ctx, proposalTxs, next)
if rf, ok := ret.Get(0).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) proposals.Proposal); ok {
r0 = rf(ctx, proposal, partialProposal, next)
} else {
r0 = ret.Get(0).(types.Context)
r0 = ret.Get(0).(proposals.Proposal)
}
if rf, ok := ret.Get(1).(func(types.Context, []types.Tx, block.ProcessLanesHandler) error); ok {
r1 = rf(ctx, proposalTxs, next)
if rf, ok := ret.Get(1).(func(types.Context, proposals.Proposal, [][]byte, block.ProcessLanesHandler) error); ok {
r1 = rf(ctx, proposal, partialProposal, next)
} else {
r1 = ret.Error(1)
}
@@ -246,8 +232,7 @@ func (_m *Lane) SetIgnoreList(ignoreList []block.Lane) {
func NewLane(t interface {
mock.TestingT
Cleanup(func())
},
) *Lane {
}) *Lane {
mock := &Lane{}
mock.Mock.Test(t)
@@ -107,8 +107,7 @@ func (_m *LaneMempool) Select(_a0 context.Context, _a1 [][]byte) mempool.Iterato
func NewLaneMempool(t interface {
mock.TestingT
Cleanup(func())
},
) *LaneMempool {
}) *LaneMempool {
mock := &LaneMempool{}
mock.Mock.Test(t)
-205
View File
@@ -1,205 +0,0 @@
package block
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"cosmossdk.io/log"
"cosmossdk.io/math"
"github.com/skip-mev/block-sdk/block/utils"
)
var _ BlockProposal = (*Proposal)(nil)
type (
// LaneProposal defines the interface/APIs that are required for the proposal to interact
// with a lane.
LaneProposal interface {
// Logger returns the lane's logger.
Logger() log.Logger
// GetMaxBlockSpace returns the maximum block space for the lane as a relative percentage.
GetMaxBlockSpace() math.LegacyDec
// Name returns the name of the lane.
Name() string
}
// BlockProposal is the interface/APIs that are required for proposal creation + interacting with
// and updating proposals. BlockProposals are iteratively updated as each lane prepares its
// partial proposal. Each lane must call UpdateProposal with its partial proposal in PrepareLane. BlockProposals
// can also include vote extensions, which are included at the top of the proposal.
BlockProposal interface { //nolint
// UpdateProposal updates the proposal with the given transactions. There are a
// few invarients that are checked:
// 1. The total size of the proposal must be less than the maximum number of bytes allowed.
// 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for
// the lane.
UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte) error
// GetMaxTxBytes returns the maximum number of bytes that can be included in the proposal.
GetMaxTxBytes() int64
// GetTotalTxBytes returns the total number of bytes currently included in the proposal.
GetTotalTxBytes() int64
// GetTxs returns the transactions in the proposal.
GetTxs() [][]byte
// GetNumTxs returns the number of transactions in the proposal.
GetNumTxs() int
// Contains returns true if the proposal contains the given transaction.
Contains(tx []byte) bool
// AddVoteExtension adds a vote extension to the proposal.
AddVoteExtension(voteExtension []byte)
// GetVoteExtensions returns the vote extensions in the proposal.
GetVoteExtensions() [][]byte
// GetProposal returns all of the transactions in the proposal along with the vote extensions
// at the top of the proposal.
GetProposal() [][]byte
}
// Proposal defines a block proposal type.
Proposal struct {
// txs is the list of transactions in the proposal.
txs [][]byte
// voteExtensions is the list of vote extensions in the proposal.
voteExtensions [][]byte
// cache is a cache of the selected transactions in the proposal.
cache map[string]struct{}
// totalTxBytes is the total number of bytes currently included in the proposal.
totalTxBytes int64
// maxTxBytes is the maximum number of bytes that can be included in the proposal.
maxTxBytes int64
}
)
// NewProposal returns a new empty proposal.
func NewProposal(maxTxBytes int64) *Proposal {
return &Proposal{
txs: make([][]byte, 0),
voteExtensions: make([][]byte, 0),
cache: make(map[string]struct{}),
maxTxBytes: maxTxBytes,
}
}
// UpdateProposal updates the proposal with the given transactions and total size. There are a
// few invarients that are checked:
// 1. The total size of the proposal must be less than the maximum number of bytes allowed.
// 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for
// the lane.
func (p *Proposal) UpdateProposal(lane LaneProposal, partialProposalTxs [][]byte) error {
if len(partialProposalTxs) == 0 {
return nil
}
partialProposalSize := int64(0)
for _, tx := range partialProposalTxs {
partialProposalSize += int64(len(tx))
}
// Invarient check: Ensure that the lane did not prepare a partial proposal that is too large.
maxTxBytesForLane := utils.GetMaxTxBytesForLane(p.GetMaxTxBytes(), p.GetTotalTxBytes(), lane.GetMaxBlockSpace())
if partialProposalSize > maxTxBytesForLane {
return fmt.Errorf(
"%s lane prepared a partial proposal that is too large: %d > %d",
lane.Name(),
partialProposalSize,
maxTxBytesForLane,
)
}
// Invarient check: Ensure that the lane did not prepare a block proposal that is too large.
updatedSize := p.totalTxBytes + partialProposalSize
if updatedSize > p.maxTxBytes {
return fmt.Errorf(
"lane %s prepared a block proposal that is too large: %d > %d",
lane.Name(),
p.totalTxBytes,
p.maxTxBytes,
)
}
p.totalTxBytes = updatedSize
p.txs = append(p.txs, partialProposalTxs...)
for _, tx := range partialProposalTxs {
txHash := sha256.Sum256(tx)
txHashStr := hex.EncodeToString(txHash[:])
p.cache[txHashStr] = struct{}{}
lane.Logger().Info(
"adding transaction to proposal",
"lane", lane.Name(),
"tx_hash", txHashStr,
"tx_bytes", len(tx),
)
}
lane.Logger().Info(
"lane successfully updated proposal",
"lane", lane.Name(),
"num_txs", len(partialProposalTxs),
"partial_proposal_size", partialProposalSize,
"cumulative_proposal_size", updatedSize,
)
return nil
}
// GetProposal returns all of the transactions in the proposal along with the vote extensions
// at the top of the proposal.
func (p *Proposal) GetProposal() [][]byte {
return append(p.voteExtensions, p.txs...)
}
// AddVoteExtension adds a vote extension to the proposal.
func (p *Proposal) AddVoteExtension(voteExtension []byte) {
p.voteExtensions = append(p.voteExtensions, voteExtension)
}
// GetVoteExtensions returns the vote extensions in the proposal.
func (p *Proposal) GetVoteExtensions() [][]byte {
return p.voteExtensions
}
// GetMaxTxBytes returns the maximum number of bytes that can be included in the proposal.
func (p *Proposal) GetMaxTxBytes() int64 {
return p.maxTxBytes
}
// GetTotalTxBytes returns the total number of bytes currently included in the proposal.
func (p *Proposal) GetTotalTxBytes() int64 {
return p.totalTxBytes
}
// GetTxs returns the transactions in the proposal.
func (p *Proposal) GetTxs() [][]byte {
return p.txs
}
// GetNumTxs returns the number of transactions in the proposal.
func (p *Proposal) GetNumTxs() int {
return len(p.txs)
}
// Contains returns true if the proposal contains the given transaction.
func (p *Proposal) Contains(tx []byte) bool {
txHash := sha256.Sum256(tx)
txHashStr := hex.EncodeToString(txHash[:])
_, ok := p.cache[txHashStr]
return ok
}
+94
View File
@@ -0,0 +1,94 @@
package proposals
import (
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block/proposals/types"
)
type (
// Proposal defines a block proposal type.
Proposal struct {
// Txs is the list of transactions in the proposal.
Txs [][]byte
// Cache is a cache of the selected transactions in the proposal.
Cache map[string]struct{}
// TxEncoder is the transaction encoder.
TxEncoder sdk.TxEncoder
// Info contains information about the state of the proposal.
Info types.ProposalInfo
}
)
// NewProposal returns a new empty proposal. Any transactions added to the proposal
// will be subject to the given max block size and max gas limit.
func NewProposal(txEncoder sdk.TxEncoder, maxBlockSize int64, maxGasLimit uint64) Proposal {
return Proposal{
TxEncoder: txEncoder,
Txs: make([][]byte, 0),
Cache: make(map[string]struct{}),
Info: types.ProposalInfo{
TxsByLane: make(map[string]uint64),
MaxBlockSize: maxBlockSize,
MaxGasLimit: maxGasLimit,
},
}
}
// GetProposalWithInfo returns all of the transactions in the proposal along with information
// about the lanes that built the proposal.
func (p *Proposal) GetProposalWithInfo() ([][]byte, error) {
// Marshall the proposal info into the first slot of the proposal.
infoBz, err := p.Info.Marshal()
if err != nil {
return nil, err
}
proposal := [][]byte{infoBz}
proposal = append(proposal, p.Txs...)
return proposal, nil
}
// GetLaneLimits returns the maximum number of bytes and gas limit that can be
// included/consumed in the proposal for the given block space ratio. Lane's
// must first call this function to determine the maximum number of bytes and
// gas limit they can include in the proposal before constructing a partial
// proposal.
func (p *Proposal) GetLaneLimits(ratio math.LegacyDec) LaneLimits {
var (
txBytes int64
gasLimit uint64
)
// In the case where the ratio is zero, we return the max tx bytes remaining.
// Note, the only lane that should have a ratio of zero is the default lane.
if ratio.IsZero() {
txBytes = p.Info.MaxBlockSize - p.Info.BlockSize
if txBytes < 0 {
txBytes = 0
}
// Unsigned subtraction needs an additional check
if p.Info.GasLimit >= p.Info.MaxGasLimit {
gasLimit = 0
} else {
gasLimit = p.Info.MaxGasLimit - p.Info.GasLimit
}
} else {
// Otherwise, we calculate the max tx bytes / gas limit for the lane based on the ratio.
txBytes = ratio.MulInt64(p.Info.MaxBlockSize).TruncateInt().Int64()
gasLimit = ratio.MulInt(math.NewIntFromUint64(p.Info.MaxGasLimit)).TruncateInt().Uint64()
}
return LaneLimits{
MaxTxBytes: txBytes,
MaxGasLimit: gasLimit,
}
}
// Contains returns true if the proposal contains the given transaction.
func (p *Proposal) Contains(txHash string) bool {
_, ok := p.Cache[txHash]
return ok
}
+540
View File
@@ -0,0 +1,540 @@
package proposals_test
import (
"math/rand"
"testing"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block/mocks"
"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"
"github.com/skip-mev/block-sdk/testutils"
"github.com/stretchr/testify/require"
)
func TestUpdateProposal(t *testing.T) {
encodingConfig := testutils.CreateTestEncodingConfig()
// Create a few random accounts
random := rand.New(rand.NewSource(1))
accounts := testutils.RandomAccounts(random, 5)
lane := mocks.NewLane(t)
lane.On("Name").Return("test").Maybe()
lane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe()
t.Run("can update with no transactions", func(t *testing.T) {
proposal := proposals.NewProposal(nil, 100, 100)
err := proposal.UpdateProposal(lane, nil)
require.NoError(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 0, len(proposal.Txs))
require.Equal(t, int64(0), proposal.Info.BlockSize)
require.Equal(t, uint64(0), proposal.Info.GasLimit)
require.Equal(t, 0, len(proposal.Info.TxsByLane))
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 1, len(block))
})
t.Run("can update with a single transaction", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := len(txBzs[0])
gasLimit := 100
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit))
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.NoError(t, err)
// Ensure that the proposal is not empty.
require.Equal(t, 1, len(proposal.Txs))
require.Equal(t, int64(size), proposal.Info.BlockSize)
require.Equal(t, uint64(gasLimit), proposal.Info.GasLimit)
require.Equal(t, 1, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"])
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 2, len(block))
require.Equal(t, txBzs[0], block[1])
})
t.Run("can update with multiple transactions", func(t *testing.T) {
txs := make([]sdk.Tx, 0)
for i := 0; i < 10; i++ {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
uint64(i),
0,
100,
)
require.NoError(t, err)
txs = append(txs, tx)
}
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), txs)
require.NoError(t, err)
size := 0
gasLimit := uint64(0)
for _, txBz := range txBzs {
size += len(txBz)
gasLimit += 100
}
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), gasLimit)
err = proposal.UpdateProposal(lane, txs)
require.NoError(t, err)
// Ensure that the proposal is not empty.
require.Equal(t, len(txs), len(proposal.Txs))
require.Equal(t, int64(size), proposal.Info.BlockSize)
require.Equal(t, gasLimit, proposal.Info.GasLimit)
require.Equal(t, uint64(10), proposal.Info.TxsByLane["test"])
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 11, len(block))
for i := 0; i < 10; i++ {
require.Equal(t, txBzs[i], block[i+1])
}
})
t.Run("rejects an update with duplicate transactions", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := int64(len(txBzs[0]))
gasLimit := uint64(100)
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), size, gasLimit)
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.NoError(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 1, len(proposal.Txs))
require.Equal(t, size, proposal.Info.BlockSize)
require.Equal(t, gasLimit, proposal.Info.GasLimit)
require.Equal(t, 1, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"])
otherlane := mocks.NewLane(t)
otherlane.On("Name").Return("test").Maybe()
otherlane.On("GetMaxBlockSpace").Return(math.LegacyNewDec(1)).Maybe()
// Attempt to add the same transaction again.
err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx})
require.Error(t, err)
require.Equal(t, 1, len(proposal.Txs))
require.Equal(t, size, proposal.Info.BlockSize)
require.Equal(t, gasLimit, proposal.Info.GasLimit)
require.Equal(t, 1, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"])
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 2, len(block))
require.Equal(t, txBzs[0], block[1])
})
t.Run("rejects an update with duplicate lane updates", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
tx2, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[1],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx, tx2})
require.NoError(t, err)
size := len(txBzs[0]) + len(txBzs[1])
gasLimit := 200
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit))
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.NoError(t, err)
err = proposal.UpdateProposal(lane, []sdk.Tx{tx2})
require.Error(t, err)
// Ensure that the proposal is not empty.
require.Equal(t, 1, len(proposal.Txs))
require.Equal(t, int64(len(txBzs[0])), proposal.Info.BlockSize)
require.Equal(t, uint64(100), proposal.Info.GasLimit)
require.Equal(t, 1, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"])
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 2, len(block))
require.Equal(t, txBzs[0], block[1])
})
t.Run("rejects an update where lane limit is smaller (block size)", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := len(txBzs[0])
gasLimit := 100
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit))
lane := mocks.NewLane(t)
lane.On("Name").Return("test").Maybe()
lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe()
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.Error(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 0, len(proposal.Txs))
require.Equal(t, int64(0), proposal.Info.BlockSize)
require.Equal(t, uint64(0), proposal.Info.GasLimit)
require.Equal(t, 0, len(proposal.Info.TxsByLane))
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 1, len(block))
})
t.Run("rejects an update where the lane limit is smaller (gas limit)", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := len(txBzs[0])
gasLimit := 100
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit))
lane := mocks.NewLane(t)
lane.On("Name").Return("test").Maybe()
lane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("0.5")).Maybe()
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.Error(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 0, len(proposal.Txs))
require.Equal(t, int64(0), proposal.Info.BlockSize)
require.Equal(t, 0, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(0), proposal.Info.GasLimit)
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 1, len(block))
})
t.Run("rejects an update where the proposal exceeds max block size", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := len(txBzs[0])
gasLimit := 100
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size)-1, uint64(gasLimit))
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.Error(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 0, len(proposal.Txs))
require.Equal(t, int64(0), proposal.Info.BlockSize)
require.Equal(t, uint64(0), proposal.Info.GasLimit)
require.Equal(t, 0, len(proposal.Info.TxsByLane))
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 1, len(block))
})
t.Run("rejects an update where the proposal exceeds max gas limit", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx})
require.NoError(t, err)
size := len(txBzs[0])
gasLimit := 100
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), int64(size), uint64(gasLimit)-1)
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.Error(t, err)
// Ensure that the proposal is empty.
require.Equal(t, 0, len(proposal.Txs))
require.Equal(t, int64(0), proposal.Info.BlockSize)
require.Equal(t, uint64(0), proposal.Info.GasLimit)
require.Equal(t, 0, len(proposal.Info.TxsByLane))
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 1, len(block))
})
t.Run("can add transactions from multiple lanes", func(t *testing.T) {
tx, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[0],
0,
1,
0,
100,
)
require.NoError(t, err)
tx2, err := testutils.CreateRandomTx(
encodingConfig.TxConfig,
accounts[1],
0,
1,
0,
100,
)
require.NoError(t, err)
txBzs, err := utils.GetEncodedTxs(encodingConfig.TxConfig.TxEncoder(), []sdk.Tx{tx, tx2})
require.NoError(t, err)
proposal := proposals.NewProposal(encodingConfig.TxConfig.TxEncoder(), 10000, 10000)
err = proposal.UpdateProposal(lane, []sdk.Tx{tx})
require.NoError(t, err)
otherlane := mocks.NewLane(t)
otherlane.On("Name").Return("test2")
otherlane.On("GetMaxBlockSpace").Return(math.LegacyMustNewDecFromStr("1.0"))
err = proposal.UpdateProposal(otherlane, []sdk.Tx{tx2})
require.NoError(t, err)
size := len(txBzs[0]) + len(txBzs[1])
gasLimit := 200
// Ensure that the proposal is not empty.
require.Equal(t, 2, len(proposal.Txs))
require.Equal(t, int64(size), proposal.Info.BlockSize)
require.Equal(t, uint64(gasLimit), proposal.Info.GasLimit)
require.Equal(t, 2, len(proposal.Info.TxsByLane))
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test"])
require.Equal(t, uint64(1), proposal.Info.TxsByLane["test2"])
// Ensure that the proposal can be marshalled.
block, err := proposal.GetProposalWithInfo()
require.NoError(t, err)
require.Equal(t, 3, len(block))
require.Equal(t, txBzs[0], block[1])
require.Equal(t, txBzs[1], block[2])
})
}
func TestGetLaneLimits(t *testing.T) {
testCases := []struct {
name string
maxTxBytes int64
totalTxBytesUsed int64
maxGasLimit uint64
totalGasLimitUsed uint64
ratio math.LegacyDec
expectedTxBytes int64
expectedGasLimit uint64
}{
{
"ratio is zero",
100,
50,
100,
50,
math.LegacyZeroDec(),
50,
50,
},
{
"ratio is zero",
100,
100,
50,
25,
math.LegacyZeroDec(),
0,
25,
},
{
"ratio is zero",
100,
150,
100,
150,
math.LegacyZeroDec(),
0,
0,
},
{
"ratio is 1",
100,
0,
75,
0,
math.LegacyOneDec(),
100,
75,
},
{
"ratio is 10%",
100,
0,
75,
0,
math.LegacyMustNewDecFromStr("0.1"),
10,
7,
},
{
"ratio is 25%",
100,
0,
80,
0,
math.LegacyMustNewDecFromStr("0.25"),
25,
20,
},
{
"ratio is 50%",
101,
0,
75,
0,
math.LegacyMustNewDecFromStr("0.5"),
50,
37,
},
{
"ratio is 33%",
100,
0,
75,
0,
math.LegacyMustNewDecFromStr("0.33"),
33,
24,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
proposal := proposals.Proposal{
Info: types.ProposalInfo{
MaxBlockSize: tc.maxTxBytes,
BlockSize: tc.totalTxBytesUsed,
MaxGasLimit: tc.maxGasLimit,
GasLimit: tc.totalGasLimitUsed,
},
}
res := proposal.GetLaneLimits(tc.ratio)
if res.MaxTxBytes != tc.expectedTxBytes {
t.Errorf("expected tx bytes %d, got %d", tc.expectedTxBytes, res.MaxTxBytes)
}
if res.MaxGasLimit != tc.expectedGasLimit {
t.Errorf("expected gas limit %d, got %d", tc.expectedGasLimit, res.MaxGasLimit)
}
})
}
}
+572
View File
@@ -0,0 +1,572 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: sdk/proposals/v1/types.proto
package types
import (
fmt "fmt"
proto "github.com/cosmos/gogoproto/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// ProposalInfo contains the metadata about a given proposal that was built by
// the block-sdk. This is used to verify and consilidate proposal data across
// the network.
type ProposalInfo struct {
// TxsByLane contains information about how each partial proposal
// was constructed by the block-sdk lanes.
TxsByLane map[string]uint64 `protobuf:"bytes,1,rep,name=txs_by_lane,json=txsByLane,proto3" json:"txs_by_lane,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
// MaxBlockSize corresponds to the upper bound on the size of the
// block that was used to construct this block proposal.
MaxBlockSize int64 `protobuf:"varint,2,opt,name=max_block_size,json=maxBlockSize,proto3" json:"max_block_size,omitempty"`
// MaxGasLimit corresponds to the upper bound on the gas limit of the
// block that was used to construct this block proposal.
MaxGasLimit uint64 `protobuf:"varint,3,opt,name=max_gas_limit,json=maxGasLimit,proto3" json:"max_gas_limit,omitempty"`
// BlockSize corresponds to the size of this block proposal.
BlockSize int64 `protobuf:"varint,4,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"`
// GasLimit corresponds to the gas limit of this block proposal.
GasLimit uint64 `protobuf:"varint,5,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty"`
}
func (m *ProposalInfo) Reset() { *m = ProposalInfo{} }
func (m *ProposalInfo) String() string { return proto.CompactTextString(m) }
func (*ProposalInfo) ProtoMessage() {}
func (*ProposalInfo) Descriptor() ([]byte, []int) {
return fileDescriptor_b5d6b8540ee6bc1e, []int{0}
}
func (m *ProposalInfo) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *ProposalInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_ProposalInfo.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *ProposalInfo) XXX_Merge(src proto.Message) {
xxx_messageInfo_ProposalInfo.Merge(m, src)
}
func (m *ProposalInfo) XXX_Size() int {
return m.Size()
}
func (m *ProposalInfo) XXX_DiscardUnknown() {
xxx_messageInfo_ProposalInfo.DiscardUnknown(m)
}
var xxx_messageInfo_ProposalInfo proto.InternalMessageInfo
func (m *ProposalInfo) GetTxsByLane() map[string]uint64 {
if m != nil {
return m.TxsByLane
}
return nil
}
func (m *ProposalInfo) GetMaxBlockSize() int64 {
if m != nil {
return m.MaxBlockSize
}
return 0
}
func (m *ProposalInfo) GetMaxGasLimit() uint64 {
if m != nil {
return m.MaxGasLimit
}
return 0
}
func (m *ProposalInfo) GetBlockSize() int64 {
if m != nil {
return m.BlockSize
}
return 0
}
func (m *ProposalInfo) GetGasLimit() uint64 {
if m != nil {
return m.GasLimit
}
return 0
}
func init() {
proto.RegisterType((*ProposalInfo)(nil), "sdk.proposals.v1.ProposalInfo")
proto.RegisterMapType((map[string]uint64)(nil), "sdk.proposals.v1.ProposalInfo.TxsByLaneEntry")
}
func init() { proto.RegisterFile("sdk/proposals/v1/types.proto", fileDescriptor_b5d6b8540ee6bc1e) }
var fileDescriptor_b5d6b8540ee6bc1e = []byte{
// 325 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x91, 0xcf, 0x4b, 0xc3, 0x30,
0x1c, 0xc5, 0x97, 0x75, 0x13, 0x9b, 0xcd, 0x31, 0x82, 0x87, 0xe2, 0x8f, 0x52, 0x86, 0x87, 0x5e,
0x96, 0x32, 0x77, 0x11, 0xf1, 0x34, 0x10, 0x11, 0x36, 0x90, 0xea, 0xc9, 0x4b, 0x49, 0xb7, 0x38,
0x43, 0x9b, 0xa6, 0x2c, 0x59, 0x69, 0xf7, 0x57, 0xf8, 0x2f, 0xf8, 0xdf, 0x78, 0xdc, 0xd1, 0xa3,
0x6c, 0xff, 0x88, 0x34, 0x9b, 0x73, 0x7a, 0xfb, 0xbe, 0x97, 0x7c, 0x1e, 0x0f, 0x1e, 0x3c, 0x93,
0x93, 0xc8, 0x4b, 0x67, 0x22, 0x15, 0x92, 0xc4, 0xd2, 0xcb, 0x7a, 0x9e, 0x2a, 0x52, 0x2a, 0x71,
0x3a, 0x13, 0x4a, 0xa0, 0xb6, 0x9c, 0x44, 0x78, 0xf7, 0x8a, 0xb3, 0x5e, 0xe7, 0xbd, 0x0a, 0x9b,
0x0f, 0x5b, 0xe3, 0x3e, 0x79, 0x11, 0x68, 0x04, 0x1b, 0x2a, 0x97, 0x41, 0x58, 0x04, 0x31, 0x49,
0xa8, 0x05, 0x1c, 0xc3, 0x6d, 0x5c, 0x76, 0xf1, 0x7f, 0x10, 0xef, 0x43, 0xf8, 0x29, 0x97, 0x83,
0x62, 0x48, 0x12, 0x7a, 0x9b, 0xa8, 0x59, 0xe1, 0x9b, 0xea, 0x47, 0xa3, 0x0b, 0xd8, 0xe2, 0x24,
0x0f, 0xc2, 0x58, 0x8c, 0xa3, 0x40, 0xb2, 0x05, 0xb5, 0xaa, 0x0e, 0x70, 0x0d, 0xbf, 0xc9, 0x49,
0x3e, 0x28, 0xcd, 0x47, 0xb6, 0xa0, 0xa8, 0x03, 0x8f, 0xca, 0x5f, 0x53, 0x22, 0x83, 0x98, 0x71,
0xa6, 0x2c, 0xc3, 0x01, 0x6e, 0xcd, 0x6f, 0x70, 0x92, 0xdf, 0x11, 0x39, 0x2c, 0x2d, 0x74, 0x0e,
0xe1, 0x5e, 0x4a, 0x4d, 0xa7, 0x98, 0xe1, 0x2e, 0xe2, 0x14, 0x9a, 0xbf, 0x78, 0x5d, 0xe3, 0x87,
0xd3, 0x2d, 0x7b, 0x72, 0x03, 0x5b, 0x7f, 0x2b, 0xa2, 0x36, 0x34, 0x22, 0x5a, 0x58, 0xc0, 0x01,
0xae, 0xe9, 0x97, 0x27, 0x3a, 0x86, 0xf5, 0x8c, 0xc4, 0xf3, 0x4d, 0xc1, 0x9a, 0xbf, 0x11, 0xd7,
0xd5, 0x2b, 0x30, 0x18, 0x7d, 0xac, 0x6c, 0xb0, 0x5c, 0xd9, 0xe0, 0x6b, 0x65, 0x83, 0xb7, 0xb5,
0x5d, 0x59, 0xae, 0xed, 0xca, 0xe7, 0xda, 0xae, 0x3c, 0xf7, 0xa7, 0x4c, 0xbd, 0xce, 0x43, 0x3c,
0x16, 0xdc, 0x93, 0x11, 0x4b, 0xbb, 0x9c, 0x66, 0x9e, 0xee, 0xd4, 0x2d, 0x77, 0xd0, 0xd7, 0xde,
0x1a, 0x7a, 0x8a, 0xf0, 0x40, 0x6f, 0xd1, 0xff, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xfb, 0x40,
0x73, 0xab, 0x01, 0x00, 0x00,
}
func (m *ProposalInfo) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ProposalInfo) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *ProposalInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.GasLimit != 0 {
i = encodeVarintTypes(dAtA, i, uint64(m.GasLimit))
i--
dAtA[i] = 0x28
}
if m.BlockSize != 0 {
i = encodeVarintTypes(dAtA, i, uint64(m.BlockSize))
i--
dAtA[i] = 0x20
}
if m.MaxGasLimit != 0 {
i = encodeVarintTypes(dAtA, i, uint64(m.MaxGasLimit))
i--
dAtA[i] = 0x18
}
if m.MaxBlockSize != 0 {
i = encodeVarintTypes(dAtA, i, uint64(m.MaxBlockSize))
i--
dAtA[i] = 0x10
}
if len(m.TxsByLane) > 0 {
for k := range m.TxsByLane {
v := m.TxsByLane[k]
baseI := i
i = encodeVarintTypes(dAtA, i, uint64(v))
i--
dAtA[i] = 0x10
i -= len(k)
copy(dAtA[i:], k)
i = encodeVarintTypes(dAtA, i, uint64(len(k)))
i--
dAtA[i] = 0xa
i = encodeVarintTypes(dAtA, i, uint64(baseI-i))
i--
dAtA[i] = 0xa
}
}
return len(dAtA) - i, nil
}
func encodeVarintTypes(dAtA []byte, offset int, v uint64) int {
offset -= sovTypes(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}
func (m *ProposalInfo) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if len(m.TxsByLane) > 0 {
for k, v := range m.TxsByLane {
_ = k
_ = v
mapEntrySize := 1 + len(k) + sovTypes(uint64(len(k))) + 1 + sovTypes(uint64(v))
n += mapEntrySize + 1 + sovTypes(uint64(mapEntrySize))
}
}
if m.MaxBlockSize != 0 {
n += 1 + sovTypes(uint64(m.MaxBlockSize))
}
if m.MaxGasLimit != 0 {
n += 1 + sovTypes(uint64(m.MaxGasLimit))
}
if m.BlockSize != 0 {
n += 1 + sovTypes(uint64(m.BlockSize))
}
if m.GasLimit != 0 {
n += 1 + sovTypes(uint64(m.GasLimit))
}
return n
}
func sovTypes(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
func sozTypes(x uint64) (n int) {
return sovTypes(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *ProposalInfo) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ProposalInfo: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ProposalInfo: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field TxsByLane", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthTypes
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthTypes
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.TxsByLane == nil {
m.TxsByLane = make(map[string]uint64)
}
var mapkey string
var mapvalue uint64
for iNdEx < postIndex {
entryPreIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
if fieldNum == 1 {
var stringLenmapkey uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLenmapkey |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLenmapkey := int(stringLenmapkey)
if intStringLenmapkey < 0 {
return ErrInvalidLengthTypes
}
postStringIndexmapkey := iNdEx + intStringLenmapkey
if postStringIndexmapkey < 0 {
return ErrInvalidLengthTypes
}
if postStringIndexmapkey > l {
return io.ErrUnexpectedEOF
}
mapkey = string(dAtA[iNdEx:postStringIndexmapkey])
iNdEx = postStringIndexmapkey
} else if fieldNum == 2 {
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
mapvalue |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
} else {
iNdEx = entryPreIndex
skippy, err := skipTypes(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthTypes
}
if (iNdEx + skippy) > postIndex {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
m.TxsByLane[mapkey] = mapvalue
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxBlockSize", wireType)
}
m.MaxBlockSize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.MaxBlockSize |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxGasLimit", wireType)
}
m.MaxGasLimit = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.MaxGasLimit |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field BlockSize", wireType)
}
m.BlockSize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.BlockSize |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field GasLimit", wireType)
}
m.GasLimit = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowTypes
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.GasLimit |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipTypes(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthTypes
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipTypes(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowTypes
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowTypes
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
case 1:
iNdEx += 8
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowTypes
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthTypes
}
iNdEx += length
case 3:
depth++
case 4:
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupTypes
}
depth--
case 5:
iNdEx += 4
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthTypes
}
if depth == 0 {
return iNdEx, nil
}
}
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthTypes = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupTypes = fmt.Errorf("proto: unexpected end of group")
)
+113
View File
@@ -0,0 +1,113 @@
package proposals
import (
"fmt"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block/utils"
)
// Lane defines the contract interface for a lane.
type Lane interface {
GetMaxBlockSpace() math.LegacyDec
Name() string
}
// UpdateProposal updates the proposal with the given transactions and lane limits. There are a
// few invariants that are checked:
// 1. The total size of the proposal must be less than the maximum number of bytes allowed.
// 2. The total size of the partial proposal must be less than the maximum number of bytes allowed for
// the lane.
// 3. The total gas limit of the proposal must be less than the maximum gas limit allowed.
// 4. The total gas limit of the partial proposal must be less than the maximum gas limit allowed for
// the lane.
// 5. The lane must not have already prepared a partial proposal.
// 6. The transaction must not already be in the proposal.
func (p *Proposal) UpdateProposal(lane Lane, partialProposal []sdk.Tx) error {
if len(partialProposal) == 0 {
return nil
}
// invariant check: Ensure we have not already prepared a partial proposal for this lane.
if _, ok := p.Info.TxsByLane[lane.Name()]; ok {
return fmt.Errorf("lane %s already prepared a partial proposal", lane)
}
// Aggregate info from the transactions.
hashes := make(map[string]struct{})
txs := make([][]byte, len(partialProposal))
partialProposalSize := int64(0)
partialProposalGasLimit := uint64(0)
for index, tx := range partialProposal {
txInfo, err := utils.GetTxInfo(p.TxEncoder, tx)
if err != nil {
return fmt.Errorf("err retrieving transaction info: %s", err)
}
// invariant check: Ensure that the transaction is not already in the proposal.
if _, ok := p.Cache[txInfo.Hash]; ok {
return fmt.Errorf("transaction %s is already in the proposal", txInfo.Hash)
}
hashes[txInfo.Hash] = struct{}{}
partialProposalSize += txInfo.Size
partialProposalGasLimit += txInfo.GasLimit
txs[index] = txInfo.TxBytes
}
// invariant check: Ensure that the partial proposal is not too large.
limit := p.GetLaneLimits(lane.GetMaxBlockSpace())
if partialProposalSize > limit.MaxTxBytes {
return fmt.Errorf(
"partial proposal is too large: %d > %d",
partialProposalSize,
limit.MaxTxBytes,
)
}
// invariant check: Ensure that the partial proposal does not consume too much gas.
if partialProposalGasLimit > limit.MaxGasLimit {
return fmt.Errorf(
"partial proposal consumes too much gas: %d > %d",
partialProposalGasLimit,
limit.MaxGasLimit,
)
}
// invariant check: Ensure that the lane did not prepare a block proposal that is too large.
updatedSize := p.Info.BlockSize + partialProposalSize
if updatedSize > p.Info.MaxBlockSize {
return fmt.Errorf(
"block proposal is too large: %d > %d",
updatedSize,
p.Info.MaxBlockSize,
)
}
// invariant check: Ensure that the lane did not prepare a block proposal that consumes too much gas.
updatedGasLimit := p.Info.GasLimit + partialProposalGasLimit
if updatedGasLimit > p.Info.MaxGasLimit {
return fmt.Errorf(
"block proposal consumes too much gas: %d > %d",
updatedGasLimit,
p.Info.MaxGasLimit,
)
}
// Update the proposal.
p.Info.BlockSize = updatedSize
p.Info.GasLimit = updatedGasLimit
// Update the lane info.
p.Info.TxsByLane[lane.Name()] = uint64(len(partialProposal))
// Update the proposal.
p.Txs = append(p.Txs, txs...)
for hash := range hashes {
p.Cache[hash] = struct{}{}
}
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package proposals
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// MaxUint64 is the maximum value of a uint64.
MaxUint64 = 1<<64 - 1
)
type (
// LaneLimits defines the constraints for a partial proposal. Each lane must only propose
// transactions that satisfy these constraints. Otherwise the partial proposal update will
// be rejected.
LaneLimits struct {
// MaxTxBytes is the maximum number of bytes allowed in the partial proposal.
MaxTxBytes int64
// MaxGasLimit is the maximum gas limit allowed in the partial proposal.
MaxGasLimit uint64
}
)
// GetBlockLimits retrieves the maximum number of bytes and gas limit allowed in a block.
func GetBlockLimits(ctx sdk.Context) (int64, uint64) {
blockParams := ctx.ConsensusParams().Block
// If the max gas is set to 0, then the max gas limit for the block can be infinite.
// Otherwise we use the max gas limit casted as a uint64 which is how gas limits are
// extracted from sdk.Tx's.
var maxGasLimit uint64
if maxGas := blockParams.MaxGas; maxGas > 0 {
maxGasLimit = uint64(maxGas)
} else {
maxGasLimit = MaxUint64
}
return blockParams.MaxBytes, maxGasLimit
}
+6 -5
View File
@@ -2,24 +2,25 @@ package block
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/skip-mev/block-sdk/block/proposals"
)
type (
// PrepareLanesHandler wraps all of the lanes' PrepareLane function into a single chained
// function. You can think of it like an AnteHandler, but for preparing proposals in the
// context of lanes instead of modules.
PrepareLanesHandler func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error)
PrepareLanesHandler func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error)
// 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, txs []sdk.Tx) (sdk.Context, error)
ProcessLanesHandler func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error)
)
// NoOpPrepareLanesHandler returns a no-op prepare lanes handler.
// This should only be used for testing.
func NoOpPrepareLanesHandler() PrepareLanesHandler {
return func(ctx sdk.Context, proposal BlockProposal) (BlockProposal, error) {
return func(ctx sdk.Context, proposal proposals.Proposal) (proposals.Proposal, error) {
return proposal, nil
}
}
@@ -27,7 +28,7 @@ func NoOpPrepareLanesHandler() PrepareLanesHandler {
// NoOpProcessLanesHandler returns a no-op process lanes handler.
// This should only be used for testing.
func NoOpProcessLanesHandler() ProcessLanesHandler {
return func(ctx sdk.Context, txs []sdk.Tx) (sdk.Context, error) {
return ctx, nil
return func(_ sdk.Context, p proposals.Proposal) (proposals.Proposal, error) {
return p, nil
}
}
+45 -26
View File
@@ -5,23 +5,47 @@ import (
"encoding/hex"
"fmt"
"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkmempool "github.com/cosmos/cosmos-sdk/types/mempool"
)
// GetTxHashStr returns the hex-encoded hash of the transaction alongside the
// transaction bytes.
func GetTxHashStr(txEncoder sdk.TxEncoder, tx sdk.Tx) ([]byte, string, error) {
type (
// TxInfo contains the information required for a transaction to be
// included in a proposal.
TxInfo struct {
// Hash is the hex-encoded hash of the transaction.
Hash string
// Size is the size of the transaction in bytes.
Size int64
// GasLimit is the gas limit of the transaction.
GasLimit uint64
// TxBytes is the bytes of the transaction.
TxBytes []byte
}
)
// GetTxHashStr returns the TxInfo of a given transaction.
func GetTxInfo(txEncoder sdk.TxEncoder, tx sdk.Tx) (TxInfo, error) {
txBz, err := txEncoder(tx)
if err != nil {
return nil, "", fmt.Errorf("failed to encode transaction: %w", err)
return TxInfo{}, fmt.Errorf("failed to encode transaction: %w", err)
}
txHash := sha256.Sum256(txBz)
txHashStr := hex.EncodeToString(txHash[:])
return txBz, txHashStr, nil
// TODO: Add an adapter to lanes so that this can be flexible to support EVM, etc.
gasTx, ok := tx.(sdk.FeeTx)
if !ok {
return TxInfo{}, fmt.Errorf("failed to cast transaction to GasTx")
}
return TxInfo{
Hash: txHashStr,
Size: int64(len(txBz)),
GasLimit: gasTx.GetGas(),
TxBytes: txBz,
}, nil
}
// GetDecodedTxs returns the decoded transactions from the given bytes.
@@ -39,6 +63,21 @@ func GetDecodedTxs(txDecoder sdk.TxDecoder, txs [][]byte) ([]sdk.Tx, error) {
return decodedTxs, nil
}
// GetEncodedTxs returns the encoded transactions from the given bytes.
func GetEncodedTxs(txEncoder sdk.TxEncoder, txs []sdk.Tx) ([][]byte, error) {
var encodedTxs [][]byte
for _, tx := range txs {
txBz, err := txEncoder(tx)
if err != nil {
return nil, fmt.Errorf("failed to encode transaction: %w", err)
}
encodedTxs = append(encodedTxs, txBz)
}
return encodedTxs, nil
}
// RemoveTxsFromLane removes the transactions from the given lane's mempool.
func RemoveTxsFromLane(txs []sdk.Tx, mempool sdkmempool.Mempool) error {
for _, tx := range txs {
@@ -49,23 +88,3 @@ func RemoveTxsFromLane(txs []sdk.Tx, mempool sdkmempool.Mempool) error {
return nil
}
// GetMaxTxBytesForLane returns the maximum number of bytes that can be included in the proposal
// for the given lane.
func GetMaxTxBytesForLane(maxTxBytes, totalTxBytes int64, ratio math.LegacyDec) int64 {
// In the case where the ratio is zero, we return the max tx bytes remaining. Note, the only
// lane that should have a ratio of zero is the default lane. This means the default lane
// will have no limit on the number of transactions it can include in a block and is only
// limited by the maxTxBytes included in the PrepareProposalRequest.
if ratio.IsZero() {
remainder := maxTxBytes - totalTxBytes
if remainder < 0 {
return 0
}
return remainder
}
// Otherwise, we calculate the max tx bytes for the lane based on the ratio.
return ratio.MulInt64(maxTxBytes).TruncateInt().Int64()
}
-78
View File
@@ -1,78 +0,0 @@
package utils_test
import (
"testing"
"cosmossdk.io/math"
"github.com/skip-mev/block-sdk/block/utils"
)
func TestGetMaxTxBytesForLane(t *testing.T) {
testCases := []struct {
name string
maxTxBytes int64
totalTxBytes int64
ratio math.LegacyDec
expected int64
}{
{
"ratio is zero",
100,
50,
math.LegacyZeroDec(),
50,
},
{
"ratio is zero",
100,
100,
math.LegacyZeroDec(),
0,
},
{
"ratio is zero",
100,
150,
math.LegacyZeroDec(),
0,
},
{
"ratio is 1",
100,
50,
math.LegacyOneDec(),
100,
},
{
"ratio is 10%",
100,
50,
math.LegacyMustNewDecFromStr("0.1"),
10,
},
{
"ratio is 25%",
100,
50,
math.LegacyMustNewDecFromStr("0.25"),
25,
},
{
"ratio is 50%",
101,
50,
math.LegacyMustNewDecFromStr("0.5"),
50,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := utils.GetMaxTxBytesForLane(tc.maxTxBytes, tc.totalTxBytes, tc.ratio)
if actual != tc.expected {
t.Errorf("expected %d, got %d", tc.expected, actual)
}
})
}
}