Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 106 additions & 34 deletions test/e2e/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/cometbft/cometbft/libs/protoio"
cryptoproto "github.com/cometbft/cometbft/proto/tendermint/crypto"
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"

cmttypes "github.com/cometbft/cometbft/types"
"github.com/cometbft/cometbft/version"
)
Expand All @@ -36,6 +37,7 @@ const (
suffixChainID string = "ChainID"
suffixVoteExtHeight string = "VoteExtensionsHeight"
suffixInitialHeight string = "InitialHeight"
suffixBlobMaxBytes string = "BlobMaxBytes"
)

// Application is an ABCI application for use by end-to-end tests. It is a
Expand Down Expand Up @@ -111,6 +113,16 @@ type Config struct {
// -1 denotes it is set at genesis.
// 0 denotes it is set at InitChain.
VoteExtensionsUpdateHeight int64 `toml:"vote_extensions_update_height"`

// BlobMaxBytesUpdateHeight configures the height at which the
// blob max bytes consensus parameters are updated
// -1 means the max_bytes value is set at genesis
// 0 means the max_bytes value is set at InitChain
// >0 means the max_bytes value is set at the given height
BlobMaxBytesUpdateHeight int64 `toml:"blob_max_bytes_update_height"`

// BlobMaxBytes is the values of max bytes for blobs
BlobMaxBytes int64 `toml:"blob_max_bytes"`
}

func DefaultConfig(dir string) *Config {
Expand All @@ -129,7 +141,8 @@ func DefaultConfig(dir string) *Config {
// 2 - blob is empty ([]byte{})
// 3 - blob is variable length string that contains "BLOBXXX" where XXX is height multiplied by 0x80 in hex
// 4 - blob is the size of MaxBlobSizeBytes and contains height truncated into two bytes repeated.
func blobOracle(height int64) ([]byte, bool) {
func blobOracle(height int64, blobMaxBytesUpdateHeight int64) ([]byte, bool) {

switch height % 4 {
case 1:
truncatedHeight := byte(height % 0x100)
Expand Down Expand Up @@ -158,14 +171,14 @@ func isBlob(blob []byte) bool {
}

// CreateBlob creates a new blob for a proposal at this height.
func CreateBlob(height int64) ([]byte, bool) {
return blobOracle(height)
func CreateBlob(height int64, blobMaxBytesUpdateHeight int64) ([]byte, bool) {
return blobOracle(height, blobMaxBytesUpdateHeight)
}

func VerifyBlob(height int64, blob []byte) bool {
func VerifyBlob(height int64, blob []byte, blobMaxBytesUpdateHeight int64) bool {
// The application might have internal checks that ensures a blob is valid.
// In this test application, we know what a valid blob should look like.
validBlob, exist := blobOracle(height)
validBlob, exist := blobOracle(height, blobMaxBytesUpdateHeight)
if !exist {
// The application received a blob at a height where no blob should be.
return len(blob) == 0
Expand Down Expand Up @@ -204,6 +217,25 @@ func (app *Application) Info(context.Context, *abci.RequestInfo) (*abci.Response
}, nil
}

// Expeected to be called with params set
func (app *Application) updateBlobMaxBytes(currentHeight int64, params *cmtproto.ConsensusParams) *cmtproto.ConsensusParams {
if params == nil {
params = &cmtproto.ConsensusParams{}
}
if app.cfg.BlobMaxBytesUpdateHeight == currentHeight {
app.logger.Info("updating blob max bytes on the fly",
"current_height", currentHeight,
"blob_max_bytes_update_height", app.cfg.BlobMaxBytesUpdateHeight)
params.Blob = &cmtproto.BlobParams{
MaxBytes: app.cfg.BlobMaxBytes,
}

app.logger.Info("updating blob max bytes in app_state", "height", app.cfg.BlobMaxBytes)
app.state.Set(prefixReservedKey+suffixBlobMaxBytes, strconv.FormatInt(app.cfg.BlobMaxBytes, 10))
}
return params
}

func (app *Application) updateVoteExtensionEnableHeight(currentHeight int64) *cmtproto.ConsensusParams {
var params *cmtproto.ConsensusParams
if app.cfg.VoteExtensionsUpdateHeight == currentHeight {
Expand All @@ -224,6 +256,7 @@ func (app *Application) updateVoteExtensionEnableHeight(currentHeight int64) *cm
// Info implements ABCI.
func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain) (*abci.ResponseInitChain, error) {
var err error

app.state.initialHeight = uint64(req.InitialHeight)
if len(req.AppStateBytes) > 0 {
err = app.state.Import(0, req.AppStateBytes)
Expand All @@ -237,6 +270,7 @@ func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain)
app.state.Set(prefixReservedKey+suffixVoteExtHeight, strconv.FormatInt(req.ConsensusParams.Abci.VoteExtensionsEnableHeight, 10))
app.logger.Info("setting initial height in app_state", "initial_height", req.InitialHeight)
app.state.Set(prefixReservedKey+suffixInitialHeight, strconv.FormatInt(req.InitialHeight, 10))
app.state.Set(prefixReservedKey+suffixBlobMaxBytes, strconv.FormatInt(req.ConsensusParams.Blob.MaxBytes, 10))
// Get validators from genesis
if req.Validators != nil {
for _, val := range req.Validators {
Expand All @@ -249,6 +283,8 @@ func (app *Application) InitChain(_ context.Context, req *abci.RequestInitChain)

params := app.updateVoteExtensionEnableHeight(0)

params = app.updateBlobMaxBytes(0, params)

resp := &abci.ResponseInitChain{
ConsensusParams: params,
AppHash: app.state.GetHash(),
Expand Down Expand Up @@ -281,6 +317,10 @@ func (app *Application) CheckTx(_ context.Context, req *abci.RequestCheckTx) (*a
// It returns a boolean indicating if a blob exists (either retrieved from the simaulted network or from the
// local cache) and the blob itself.
func (app *Application) GetBlob(height int64) ([]byte, bool, error) {
if !app.checkBlobHeight(height, "getBlob") {
// Blob max bytes is still 0 so we cannot send a blob
return nil, false, nil
}
// First check the local cache
if blob, found := app.blobCache[height]; found {
if !isBlob(blob) {
Expand All @@ -291,7 +331,7 @@ func (app *Application) GetBlob(height int64) ([]byte, bool, error) {
}
// If not found, reach out to other peers and retrieve it through them.
time.Sleep(100 * time.Millisecond) // Add simulated network delay
blobFromPeer, exist := blobOracle(height)
blobFromPeer, exist := blobOracle(height, app.cfg.BlobMaxBytesUpdateHeight)
if exist && !isBlob(blobFromPeer) {
return nil, false, nil
}
Expand All @@ -317,18 +357,20 @@ func (app *Application) FinalizeBlock(_ context.Context, req *abci.RequestFinali
}

// Verify blob.
blob, exists, err := app.GetBlob(req.Height)
if err != nil {
return nil, fmt.Errorf("could not fetch blob for height %d, %s", req.Height, err.Error())
}
if exists && !VerifyBlob(req.Height, blob) {
return nil, fmt.Errorf("blob verification failed for height %d", req.Height)
}
if app.checkBlobHeight(req.Height, "FinalizeBlock") {
blob, exists, err := app.GetBlob(req.Height)
if err != nil {
return nil, fmt.Errorf("could not fetch blob for height %d, %s", req.Height, err.Error())
}
if exists && !VerifyBlob(req.Height, blob, app.cfg.BlobMaxBytesUpdateHeight) {
return nil, fmt.Errorf("blob verification failed for height %d", req.Height)
}

// This is a short-term cache so we delete the entry after we received it.
delete(app.blobCache, req.Height)
if len(app.blobCache) != 0 {
app.logger.Error("blob cache should be empty", "size", len(app.blobCache))
// This is a short-term cache so we delete the entry after we received it.
delete(app.blobCache, req.Height)
if len(app.blobCache) != 0 {
app.logger.Error("blob cache should be empty", "size", len(app.blobCache))
}
}

for _, ev := range req.Misbehavior {
Expand All @@ -348,6 +390,8 @@ func (app *Application) FinalizeBlock(_ context.Context, req *abci.RequestFinali

params := app.updateVoteExtensionEnableHeight(req.Height)

params = app.updateBlobMaxBytes(req.Height, params)

if app.cfg.FinalizeBlockDelay != 0 {
time.Sleep(app.cfg.FinalizeBlockDelay)
}
Expand Down Expand Up @@ -527,9 +571,12 @@ func (app *Application) PrepareProposal(
// Coherence: No need to call parseTx, as the check is stateless and has been performed by CheckTx
txs = append(txs, tx)
}
var blob []byte
var exists bool
// Generate blob for the current height.
blob, exists := CreateBlob(req.Height)

if app.checkBlobHeight(req.Height, "prepare_proposal") {
blob, exists = CreateBlob(req.Height, app.cfg.BlobMaxBytesUpdateHeight)
}
if app.cfg.PrepareProposalDelay != 0 {
time.Sleep(app.cfg.PrepareProposalDelay)
}
Expand All @@ -548,23 +595,23 @@ func (app *Application) PrepareProposal(
func (app *Application) ProcessProposal(_ context.Context, req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) {
r := &abci.Request{Value: &abci.Request_ProcessProposal{ProcessProposal: &abci.RequestProcessProposal{}}}
app.logger.Info("ABCIRequest", "request", r)
if app.checkBlobHeight(req.Height, "ProcessProposal") {
fmt.Println("BLOB: ", req.Blob)

fmt.Println("BLOB: ", req.Blob)

if !VerifyBlob(req.Height, req.Blob) {
app.logger.Error("invalid blob, rejecting proposal", "received height", req.Height, "received", req.Blob)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
// Blob verification
if len(req.Blob) != 0 {
// Proposal will be accepted by us and blob exists and valid, so we store it in the cache.
app.blobCache[req.Height] = req.Blob
} else {
// Proposal will be accepted by us and there is no blob.
// We make a note of it in the cache so we can inform FinalizeBlock. A real application will have better methods for this.
app.blobCache[req.Height] = noBlobBytes()
if !VerifyBlob(req.Height, req.Blob, app.cfg.BlobMaxBytesUpdateHeight) {
app.logger.Error("invalid blob, rejecting proposal", "received height", req.Height, "received", req.Blob)
return &abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_REJECT}, nil
}
// Blob verification
if len(req.Blob) != 0 {
// Proposal will be accepted by us and blob exists and valid, so we store it in the cache.
app.blobCache[req.Height] = req.Blob
} else {
// Proposal will be accepted by us and there is no blob.
// We make a note of it in the cache so we can inform FinalizeBlock. A real application will have better methods for this.
app.blobCache[req.Height] = noBlobBytes()
}
}

_, areExtensionsEnabled := app.checkHeightAndExtensions(true, req.Height, "ProcessProposal")

for _, tx := range req.Txs {
Expand Down Expand Up @@ -686,6 +733,31 @@ func (app *Application) getAppHeight() int64 {
return appHeight + 1
}

func (app *Application) checkBlobHeight(height int64, callsite string) bool {
appHeight := app.getAppHeight()
if height != appHeight {
panic(fmt.Errorf(
"got unexpected height in %s request; expected %d, actual %d",
callsite, appHeight, height,
))
}
if appHeight <= app.cfg.BlobMaxBytesUpdateHeight {
return false
}
blobMaxBytesStr := app.state.Get(prefixReservedKey + suffixBlobMaxBytes)
if len(blobMaxBytesStr) == 0 {
panic("blob max bytes not set in database")
}
blobMaxBytes, err := strconv.ParseInt(blobMaxBytesStr, 10, 64)
if err != nil {
panic(fmt.Errorf("malformed blob max bytes %q in database", blobMaxBytesStr))
}
if blobMaxBytes == 0 {
return false
}
return true
}

func (app *Application) checkHeightAndExtensions(isPrepareProcessProposal bool, height int64, callsite string) (int64, bool) {
appHeight := app.getAppHeight()
if height != appHeight {
Expand Down
13 changes: 13 additions & 0 deletions test/e2e/generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/Masterminds/semver/v3"
e2e "github.com/cometbft/cometbft/test/e2e/pkg"
"github.com/cometbft/cometbft/types"
"github.com/cometbft/cometbft/version"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
Expand Down Expand Up @@ -62,6 +63,8 @@ var (
voteExtensionUpdateHeight = uniformChoice{int64(-1), int64(0), int64(1)} // -1: genesis, 0: InitChain, 1: (use offset)
voteExtensionEnabled = weightedChoice{true: 3, false: 1}
voteExtensionHeightOffset = uniformChoice{int64(0), int64(10), int64(100)}
blobMaxBytesUpdateHeight = uniformChoice{int64(-1), int64(0), int64(1)}
blobHeightOffset = uniformChoice{int64(0), int64(10), int64(100)}
)

type generateConfig struct {
Expand Down Expand Up @@ -157,6 +160,16 @@ func generateTestnet(r *rand.Rand, opt map[string]interface{}, upgradeVersion st
manifest.VoteExtensionsEnableHeight = baseHeight + voteExtensionHeightOffset.Choose(r).(int64)
}

manifest.BlobMaxBytesUpdateHeight = blobMaxBytesUpdateHeight.Choose(r).(int64)

if manifest.BlobMaxBytesUpdateHeight == 1 {
manifest.BlobMaxBytesUpdateHeight = manifest.InitialHeight + blobHeightOffset.Choose(r).(int64)
manifest.BlobMaxBytes = types.MaxBlobSizeBytes
}

if manifest.BlobMaxBytesUpdateHeight == 0 {
manifest.BlobMaxBytes = types.MaxBlobSizeBytes
}
var numSeeds, numValidators, numFulls, numLightClients int
switch opt["topology"].(string) {
case "single":
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/networks/ci.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ ipv6 = true
initial_height = 1000
vote_extensions_update_height = -1
vote_extensions_enable_height = 1000
blob_max_bytes_update_height = 1000
blob_max_bytes = 819200
evidence = 5
initial_state = { initial01 = "a", initial02 = "b", initial03 = "c" }
prepare_proposal_delay = "100ms"
Expand Down
4 changes: 4 additions & 0 deletions test/e2e/node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Config struct {
KeyType string `toml:"key_type"`
VoteExtensionsEnableHeight int64 `toml:"vote_extensions_enable_height"`
VoteExtensionsUpdateHeight int64 `toml:"vote_extensions_update_height"`
BlobMaxBytesUpdateHeight int64 `toml:"blob_max_bytes_update_height"`
BlobMaxBytes int64 `toml:"blob_max_bytes"`
}

// App extracts out the application specific configuration parameters
Expand All @@ -39,6 +41,8 @@ func (cfg *Config) App() *app.Config {
PersistInterval: cfg.PersistInterval,
VoteExtensionsEnableHeight: cfg.VoteExtensionsEnableHeight,
VoteExtensionsUpdateHeight: cfg.VoteExtensionsUpdateHeight,
BlobMaxBytesUpdateHeight: cfg.BlobMaxBytesUpdateHeight,
BlobMaxBytes: cfg.BlobMaxBytes,
}
}

Expand Down
16 changes: 11 additions & 5 deletions test/e2e/pkg/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,6 @@ type Manifest struct {
// BlockMaxBytes specifies the maximum size in bytes of a block. This
// value will be written to the genesis file of all nodes.
BlockMaxBytes int64 `toml:"block_max_bytes"`

// BlobMaxBytes specifies the maximum size in bytes of a blob. This
// value will be written to the genesis file of all nodes.
BlobMaxBytes int64 `toml:"blob_max_bytes"`

// VoteExtensionsEnableHeight configures the first height during which
// the chain will use and require vote extension data to be present
// in precommit messages.
Expand All @@ -116,6 +111,17 @@ type Manifest struct {
// Maximum number of peers to which the node gossips transactions
ExperimentalMaxGossipConnectionsToPersistentPeers uint `toml:"experimental_max_gossip_connections_to_persistent_peers"`
ExperimentalMaxGossipConnectionsToNonPersistentPeers uint `toml:"experimental_max_gossip_connections_to_non_persistent_peers"`

// BlobMaxBytesUpdateHeight configures the height at which the
// blob max bytes consensus parameters are updated
// -1 means the max_bytes value is set at genesis
// 0 means the max_bytes value is set at InitChain
// >0 means the max_bytes value is set at the given height
BlobMaxBytesUpdateHeight int64 `toml:"blob_max_bytes_update_height"`

// BlobMaxBytes specifies the maximum size in bytes of a blob. This
// value will be written to the genesis file of all nodes.
BlobMaxBytes int64 `toml:"blob_max_bytes"`
}

// ManifestNode represents a node in a testnet manifest.
Expand Down
Loading