Skip to content

Commit

Permalink
fix: Proposal vote extensions' byte limit (#585)
Browse files Browse the repository at this point in the history
Closes babylonlabs-io/pm#250. In the PR,
PrepareProposal first adds the injected checkpoint tx and then add
normal txs by order until byte limit is hit
  • Loading branch information
gitferry authored Mar 3, 2025
1 parent d73fc91 commit 638d5ef
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ cache if an old BTC delegation receives inclusion proof
- [#525](https://github.com/babylonlabs-io/babylon/pull/525) fix: add back `NewIBCHeaderDecorator` post handler
- [#563](https://github.com/babylonlabs-io/babylon/pull/563) reject coinbase staking transactions
- [#584](https://github.com/babylonlabs-io/babylon/pull/584) fix: Panic can be triggered in handling liveness
- [#585](https://github.com/babylonlabs-io/babylon/pull/585) fix: Proposal vote extensions' byte limit
- [#594](https://github.com/babylonlabs-io/babylon/pull/594) Refund tx to correct recipient

## v1.0.0-rc6
Expand Down
6 changes: 4 additions & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ import (
"github.com/babylonlabs-io/babylon/x/btcstkconsumer"
bsctypes "github.com/babylonlabs-io/babylon/x/btcstkconsumer/types"
"github.com/babylonlabs-io/babylon/x/checkpointing"
"github.com/babylonlabs-io/babylon/x/checkpointing/prepare"
checkpointingtypes "github.com/babylonlabs-io/babylon/x/checkpointing/types"
"github.com/babylonlabs-io/babylon/x/checkpointing/vote_extensions"
"github.com/babylonlabs-io/babylon/x/epoching"
epochingtypes "github.com/babylonlabs-io/babylon/x/epoching/types"
"github.com/babylonlabs-io/babylon/x/finality"
Expand Down Expand Up @@ -501,12 +503,12 @@ func NewBabylonApp(
)

// set proposal extension
proposalHandler := checkpointing.NewProposalHandler(
proposalHandler := prepare.NewProposalHandler(
logger, &app.CheckpointingKeeper, bApp.Mempool(), bApp, app.EncCfg)
proposalHandler.SetHandlers(bApp)

// set vote extension
voteExtHandler := checkpointing.NewVoteExtensionHandler(logger, &app.CheckpointingKeeper)
voteExtHandler := vote_extensions.NewVoteExtensionHandler(logger, &app.CheckpointingKeeper)
voteExtHandler.SetHandlers(bApp)

app.SetInitChainer(app.InitChainer)
Expand Down
3 changes: 2 additions & 1 deletion test/replay/btcstaking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/babylonlabs-io/babylon/testutil/datagen"
bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types"
"github.com/stretchr/testify/require"
)

// TestEpochFinalization checks whether we can finalize some epochs
Expand Down
4 changes: 4 additions & 0 deletions testutil/helper/gen_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (h *Helper) ApplyEmptyBlockWithVoteExtension(r *rand.Rand) (sdk.Context, er
// 3. prepare proposal with previous BLS sigs
blockTxs := [][]byte{}
ppRes, err := h.App.PrepareProposal(&abci.RequestPrepareProposal{
MaxTxBytes: 10000,
LocalLastCommit: extendedCommitInfo,
Height: newHeight,
})
Expand Down Expand Up @@ -232,6 +233,7 @@ func (h *Helper) ApplyEmptyBlockWithValSet(r *rand.Rand, valSetWithKeys *datagen
// 3. prepare proposal with previous BLS sigs
blockTxs := [][]byte{}
ppRes, err := h.App.PrepareProposal(&abci.RequestPrepareProposal{
MaxTxBytes: 10000,
LocalLastCommit: abci.ExtendedCommitInfo{Votes: extendedVotes},
Height: newHeight,
})
Expand Down Expand Up @@ -333,6 +335,7 @@ func (h *Helper) ApplyEmptyBlockWithInvalidVoteExtensions(r *rand.Rand) (sdk.Con
// 3. prepare proposal with previous BLS sigs
blockTxs := [][]byte{}
ppRes, err := h.App.PrepareProposal(&abci.RequestPrepareProposal{
MaxTxBytes: 10000,
LocalLastCommit: extendedCommitInfo,
Height: newHeight,
})
Expand Down Expand Up @@ -440,6 +443,7 @@ func (h *Helper) ApplyEmptyBlockWithSomeInvalidVoteExtensions(r *rand.Rand) (sdk
var blockTxs [][]byte

ppRes, err := h.App.PrepareProposal(&abci.RequestPrepareProposal{
MaxTxBytes: 10000,
LocalLastCommit: extendedCommitInfo,
Height: newHeight,
})
Expand Down
79 changes: 52 additions & 27 deletions x/checkpointing/proposal.go → x/checkpointing/prepare/proposal.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package checkpointing
package prepare

import (
"bytes"
"encoding/hex"
"fmt"
"slices"

"cosmossdk.io/log"
abci "github.com/cometbft/cometbft/abci/types"
Expand All @@ -19,15 +18,17 @@ import (

const defaultInjectedTxIndex = 0

var (
EmptyProposalRes = abci.ResponsePrepareProposal{Txs: [][]byte{}}
)

type ProposalHandler struct {
logger log.Logger
ckptKeeper CheckpointingKeeper
bApp *baseapp.BaseApp

// used for building and parsing the injected tx
txEncoder sdk.TxEncoder
txDecoder sdk.TxDecoder
txBuilder client.TxBuilder
txConfig client.TxConfig

defaultPrepareProposalHandler sdk.PrepareProposalHandler
defaultProcessProposalHandler sdk.ProcessProposalHandler
Expand All @@ -47,9 +48,7 @@ func NewProposalHandler(
logger: logger,
ckptKeeper: ckptKeeper,
bApp: bApp,
txEncoder: encCfg.TxConfig.TxEncoder(),
txDecoder: encCfg.TxConfig.TxDecoder(),
txBuilder: encCfg.TxConfig.NewTxBuilder(),
txConfig: encCfg.TxConfig,
defaultPrepareProposalHandler: defaultHandler.PrepareProposalHandler(),
defaultProcessProposalHandler: defaultHandler.ProcessProposalHandler(),
}
Expand All @@ -74,48 +73,57 @@ func (h *ProposalHandler) PrepareProposal() sdk.PrepareProposalHandler {
}

k := h.ckptKeeper
proposalTxs := res.Txs
proposalRes := &abci.ResponsePrepareProposal{Txs: proposalTxs}
defaultProposalRes := &abci.ResponsePrepareProposal{Txs: res.Txs}

epoch := k.GetEpoch(ctx)
// BLS signatures are sent in the last block of the previous epoch,
// so they should be aggregated in the first block of the new epoch
// and no BLS signatures are send in epoch 0
if !epoch.IsVoteExtensionProposal(ctx) {
return proposalRes, nil
return defaultProposalRes, nil
}

proposalTxs, err := NewPrepareProposalTxs(req)
if err != nil {
return &EmptyProposalRes, fmt.Errorf("NewPrepareProposalTxs error: %w", err)
}

if len(req.LocalLastCommit.Votes) == 0 {
return proposalRes, fmt.Errorf("no extended votes received from the last block")
return &EmptyProposalRes, fmt.Errorf("no extended votes received from the last block")
}

// 1. verify the validity of vote extensions (2/3 majority is achieved)
err = baseapp.ValidateVoteExtensions(ctx, h.ckptKeeper, req.Height, ctx.ChainID(), req.LocalLastCommit)
if err != nil {
return proposalRes, fmt.Errorf("invalid vote extensions: %w", err)
return &EmptyProposalRes, fmt.Errorf("invalid vote extensions: %w", err)
}

// 2. build a checkpoint for the previous epoch
// Note: the epoch has not increased yet, so
// we can use the current epoch
ckpt, err := h.buildCheckpointFromVoteExtensions(ctx, epoch.EpochNumber, req.LocalLastCommit.Votes)
if err != nil {
return proposalRes, fmt.Errorf("failed to build checkpoint from vote extensions: %w", err)
return &EmptyProposalRes, fmt.Errorf("failed to build checkpoint from vote extensions: %w", err)
}

// 3. inject a "fake" tx into the proposal s.t. validators can decode, verify the checkpoint
injectedCkpt := &ckpttypes.MsgInjectedCheckpoint{
Ckpt: ckpt,
ExtendedCommitInfo: &req.LocalLastCommit,
// 3. inject a checkpoint tx into the proposal s.t. validators can decode, verify the checkpoint
injectedVoteExtTx, err := h.buildInjectedTxBytes(ckpt, &req.LocalLastCommit)
if err != nil {
return &EmptyProposalRes, fmt.Errorf("failed to encode vote extensions into a special tx: %w", err)
}
injectedVoteExtTx, err := h.buildInjectedTxBytes(injectedCkpt)

err = proposalTxs.SetOrReplaceCheckpointTx(injectedVoteExtTx)
if err != nil {
return nil, fmt.Errorf("failed to encode vote extensions into a special tx: %w", err)
return &EmptyProposalRes, fmt.Errorf("failed to inject checkpoint tx into the proposal: %w", err)
}

err = proposalTxs.ReplaceOtherTxs(res.Txs)
if err != nil {
return &EmptyProposalRes, fmt.Errorf("failed to add other txs into the proposal: %w", err)
}
proposalTxs = slices.Insert(proposalTxs, defaultInjectedTxIndex, [][]byte{injectedVoteExtTx}...)

return &abci.ResponsePrepareProposal{
Txs: proposalTxs,
Txs: proposalTxs.GetTxsInOrder(),
}, nil
}
}
Expand Down Expand Up @@ -382,12 +390,13 @@ func (h *ProposalHandler) PreBlocker() sdk.PreBlocker {
}
}

func (h *ProposalHandler) buildInjectedTxBytes(injectedCkpt *ckpttypes.MsgInjectedCheckpoint) ([]byte, error) {
if err := h.txBuilder.SetMsgs(injectedCkpt); err != nil {
return nil, err
func (h *ProposalHandler) buildInjectedTxBytes(ckpt *ckpttypes.RawCheckpointWithMeta, info *abci.ExtendedCommitInfo) ([]byte, error) {
msg := &ckpttypes.MsgInjectedCheckpoint{
Ckpt: ckpt,
ExtendedCommitInfo: info,
}

return h.txEncoder(h.txBuilder.GetTx())
return EncodeMsgsIntoTxBytes(h.txConfig, msg)
}

// ExtractInjectedCheckpoint extracts the injected checkpoint from the tx set
Expand All @@ -402,7 +411,7 @@ func (h *ProposalHandler) ExtractInjectedCheckpoint(txs [][]byte) (*ckpttypes.Ms
return nil, fmt.Errorf("the injected vote extensions tx is empty")
}

injectedTx, err := h.txDecoder(injectedTxBytes)
injectedTx, err := h.txConfig.TxDecoder()(injectedTxBytes)
if err != nil {
return nil, fmt.Errorf("failed to decode injected vote extension tx: %w", err)
}
Expand All @@ -425,3 +434,19 @@ func removeInjectedTx(txs [][]byte) ([][]byte, error) {

return txs, nil
}

// EncodeMsgsIntoTxBytes encodes the given msgs into a single transaction.
func EncodeMsgsIntoTxBytes(txConfig client.TxConfig, msgs ...sdk.Msg) ([]byte, error) {
txBuilder := txConfig.NewTxBuilder()
err := txBuilder.SetMsgs(msgs...)
if err != nil {
return nil, err
}

txBytes, err := txConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, err
}

return txBytes, nil
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package checkpointing
package prepare

import (
"context"

cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/babylonlabs-io/babylon/crypto/bls12381"
"github.com/babylonlabs-io/babylon/x/checkpointing/types"
epochingtypes "github.com/babylonlabs-io/babylon/x/epoching/types"
cmtprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)

type CheckpointingKeeper interface {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package checkpointing_test
package prepare_test

import (
"bytes"
Expand Down Expand Up @@ -27,7 +27,7 @@ import (
"github.com/babylonlabs-io/babylon/testutil/datagen"
"github.com/babylonlabs-io/babylon/testutil/helper"
"github.com/babylonlabs-io/babylon/testutil/mocks"
"github.com/babylonlabs-io/babylon/x/checkpointing"
"github.com/babylonlabs-io/babylon/x/checkpointing/prepare"
checkpointingtypes "github.com/babylonlabs-io/babylon/x/checkpointing/types"
et "github.com/babylonlabs-io/babylon/x/epoching/types"
)
Expand Down Expand Up @@ -492,7 +492,7 @@ func TestPrepareProposalAtVoteExtensionHeight(t *testing.T) {
name := t.Name()
encCfg := appparams.DefaultEncodingConfig()
bApp := baseapp.NewBaseApp(name, logger, db, encCfg.TxConfig.TxDecoder(), baseapp.SetChainID("chain-test"))
h := checkpointing.NewProposalHandler(
h := prepare.NewProposalHandler(
log.NewNopLogger(),
ek,
mem,
Expand Down
105 changes: 105 additions & 0 deletions x/checkpointing/prepare/transactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package prepare

import (
"errors"
"fmt"

abci "github.com/cometbft/cometbft/abci/types"
)

// PrepareProposalTxs is used as an intermediary storage for transactions when creating
// a proposal for `PrepareProposal`.
type PrepareProposalTxs struct {
// Transactions.
CheckpointTx []byte
OtherTxs [][]byte

// Bytes.
// In general, there's no need to check for int64 overflow given that it would require
// exabytes of memory to hit the max int64 value in bytes.
MaxBytes uint64
UsedBytes uint64
}

// NewPrepareProposalTxs returns a new `PrepareProposalTxs` given the request.
func NewPrepareProposalTxs(
req *abci.RequestPrepareProposal,
) (PrepareProposalTxs, error) {
if req.MaxTxBytes <= 0 {
return PrepareProposalTxs{}, errors.New("MaxTxBytes must be positive")
}

return PrepareProposalTxs{
MaxBytes: uint64(req.MaxTxBytes),
UsedBytes: 0,
}, nil
}

// SetOrReplaceCheckpointTx sets the tx used for checkpoint. If the checkpoint tx already exists,
// replace it
func (t *PrepareProposalTxs) SetOrReplaceCheckpointTx(tx []byte) error {
oldBytes := uint64(len(t.CheckpointTx))
newBytes := uint64(len(tx))
if err := t.updateUsedBytes(oldBytes, newBytes); err != nil {
return err
}
t.CheckpointTx = tx
return nil
}

// ReplaceOtherTxs replaces other txs with the given txs (existing ones are cleared)
func (t *PrepareProposalTxs) ReplaceOtherTxs(allTxs [][]byte) error {
t.OtherTxs = make([][]byte, 0, len(allTxs))
bytesToAdd := uint64(0)
for _, tx := range allTxs {
txSize := uint64(len(tx))
if t.UsedBytes+bytesToAdd+txSize > t.MaxBytes {
break
}

bytesToAdd += txSize
t.OtherTxs = append(t.OtherTxs, tx)
}

if err := t.updateUsedBytes(0, bytesToAdd); err != nil {
return err
}

return nil
}

// updateUsedBytes updates the used bytes field. This returns an error if the num used bytes
// exceeds the max byte limit.
func (t *PrepareProposalTxs) updateUsedBytes(
bytesToRemove uint64,
bytesToAdd uint64,
) error {
if t.UsedBytes < bytesToRemove {
return errors.New("result cannot be negative")
}

finalBytes := t.UsedBytes - bytesToRemove + bytesToAdd
if finalBytes > t.MaxBytes {
return fmt.Errorf("exceeds max: max=%d, used=%d, adding=%d", t.MaxBytes, t.UsedBytes, bytesToAdd)
}

t.UsedBytes = finalBytes
return nil
}

// GetTxsInOrder returns a list of txs in an order that the `ProcessProposal` expects.
func (t *PrepareProposalTxs) GetTxsInOrder() [][]byte {
txsToReturn := make([][]byte, 0, 1+len(t.OtherTxs))

// 1. Checkpoint tx
if len(t.CheckpointTx) > 0 {
txsToReturn = append(txsToReturn, t.CheckpointTx)
}

// 2. "Other" txs
if len(t.OtherTxs) > 0 {
txsToReturn = append(txsToReturn, t.OtherTxs...)
}

return txsToReturn
}
Loading

0 comments on commit 638d5ef

Please sign in to comment.