diff --git a/app/estimate_square_size.go b/app/estimate_square_size.go index bd9c3eeeba..22ad5b9356 100644 --- a/app/estimate_square_size.go +++ b/app/estimate_square_size.go @@ -15,7 +15,7 @@ import ( // // NOTE: The estimation process does not have to be perfect. We can overestimate // because the cost of padding is limited. -func estimateSquareSize(txs []parsedTx) (squareSize uint64, nonreserveStart int) { +func estimateSquareSize(txs []parsedTx, minSquareSize, maxSquareSize uint64) (squareSize uint64, nonreserveStart int) { txSharesUsed := estimateCompactShares(appconsts.DefaultMaxSquareSize, txs) blobSharesUsed := 0 @@ -35,11 +35,11 @@ func estimateSquareSize(txs []parsedTx) (squareSize uint64, nonreserveStart int) totalSharesUsed *= 2 minSize := uint64(math.Sqrt(float64(totalSharesUsed))) squareSize = shares.RoundUpPowerOfTwo(minSize) - if squareSize >= appconsts.DefaultMaxSquareSize { - squareSize = appconsts.DefaultMaxSquareSize + if squareSize >= maxSquareSize { + squareSize = maxSquareSize } - if squareSize <= appconsts.DefaultMinSquareSize { - squareSize = appconsts.DefaultMinSquareSize + if squareSize <= minSquareSize { + squareSize = minSquareSize } return squareSize, txSharesUsed @@ -88,3 +88,8 @@ func maxWrappedTxOverhead(squareSize uint64) int { } return len(wtx) - int(maxTxLen) } + +func isValidSquareSize(squareSize, minSquareSize, maxSquareSize int) bool { + // Check that square size is within range + return squareSize >= minSquareSize && squareSize <= maxSquareSize +} diff --git a/app/estimate_square_size_test.go b/app/estimate_square_size_test.go index e2f64190b6..8a02fe4717 100644 --- a/app/estimate_square_size_test.go +++ b/app/estimate_square_size_test.go @@ -35,7 +35,7 @@ func Test_estimateSquareSize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ptxs := generateMixedParsedTxs(tt.normalTxs, tt.pfbCount, tt.pfbSize) - res, _ := estimateSquareSize(ptxs) + res, _ := estimateSquareSize(ptxs, appconsts.DefaultMinSquareSize, appconsts.DefaultMaxSquareSize) assert.Equal(t, tt.expectedSquareSize, res) }) } diff --git a/app/prepare_proposal.go b/app/prepare_proposal.go index 5f5bb50feb..e70eec2897 100644 --- a/app/prepare_proposal.go +++ b/app/prepare_proposal.go @@ -20,7 +20,10 @@ func (app *App) PrepareProposal(req abci.RequestPrepareProposal) abci.ResponsePr // estimate the square size. This estimation errs on the side of larger // squares but can only return values within the min and max square size. - squareSize, nonreservedStart := estimateSquareSize(parsedTxs) + ctx := app.NewContext(true, core.Header{Height: app.LastBlockHeight()}) + minSquareSize := app.BlobKeeper.MinSquareSize(ctx) + maxSquareSize := app.BlobKeeper.MaxSquareSize(ctx) + squareSize, nonreservedStart := estimateSquareSize(parsedTxs, uint64(minSquareSize), uint64(maxSquareSize)) // finalizeLayout wraps any blob transactions with their final share index. // This requires sorting the blobs by namespace and potentially pruning diff --git a/app/process_proposal.go b/app/process_proposal.go index 0b94963f8d..b3752cc0c5 100644 --- a/app/process_proposal.go +++ b/app/process_proposal.go @@ -2,6 +2,7 @@ package app import ( "bytes" + "fmt" "sort" "github.com/celestiaorg/celestia-app/pkg/appconsts" @@ -27,6 +28,10 @@ func (app *App) ProcessProposal(req abci.RequestProcessProposal) abci.ResponsePr // - the commitment in each PFB should match the commitment for the shares that contain that blob data // - there should be no unpaid-for data + ctx := app.NewContext(true, tmproto.Header{Height: app.LastBlockHeight()}) + minSquareSize := app.BlobKeeper.MinSquareSize(ctx) + maxSquareSize := app.BlobKeeper.MaxSquareSize(ctx) + data, err := coretypes.DataFromProto(req.BlockData) if err != nil { logInvalidPropBlockError(app.Logger(), req.Header, "failure to unmarshal block data:", err) @@ -35,6 +40,21 @@ func (app *App) ProcessProposal(req abci.RequestProcessProposal) abci.ResponsePr } } + squareSize := data.SquareSize + + // validate proposed square size + if !isValidSquareSize(int(squareSize), int(minSquareSize), int(maxSquareSize)) { + logInvalidPropBlockError(app.Logger(), req.Header, "invalid square size:", fmt.Errorf( + "min %d max %d provided %d", + minSquareSize, + maxSquareSize, + squareSize, + )) + return abci.ResponseProcessProposal{ + Result: abci.ResponseProcessProposal_REJECT, + } + } + if !sort.IsSorted(coretypes.BlobsByNamespace(data.Blobs)) { logInvalidPropBlock(app.Logger(), req.Header, "blobs are unsorted") return abci.ResponseProcessProposal{ @@ -50,7 +70,7 @@ func (app *App) ProcessProposal(req abci.RequestProcessProposal) abci.ResponsePr } } - cacher := inclusion.NewSubtreeCacher(data.SquareSize) + cacher := inclusion.NewSubtreeCacher(squareSize) eds, err := rsmt2d.ComputeExtendedDataSquare(shares.ToBytes(dataSquare), appconsts.DefaultCodec(), cacher.Constructor) if err != nil { logInvalidPropBlockError(app.Logger(), req.Header, "failure to erasure the data square", err) diff --git a/app/test/process_proposal_test.go b/app/test/process_proposal_test.go index 8dfd8891cc..b09493473b 100644 --- a/app/test/process_proposal_test.go +++ b/app/test/process_proposal_test.go @@ -96,6 +96,22 @@ func TestBlobInclusionCheck(t *testing.T) { }, expectedResult: abci.ResponseProcessProposal_REJECT, }, + { + name: "square size greater than max square size", + input: validData(), + mutator: func(d *core.Data) { + d.SquareSize = appconsts.DefaultMaxSquareSize * 2 + }, + expectedResult: abci.ResponseProcessProposal_REJECT, + }, + { + name: "square size of zero", + input: validData(), + mutator: func(d *core.Data) { + d.SquareSize = 0 + }, + expectedResult: abci.ResponseProcessProposal_REJECT, + }, } for _, tt := range tests { diff --git a/pkg/da/data_availability_header.go b/pkg/da/data_availability_header.go index 88dea67311..818b02241f 100644 --- a/pkg/da/data_availability_header.go +++ b/pkg/da/data_availability_header.go @@ -13,11 +13,6 @@ import ( "github.com/tendermint/tendermint/types" ) -const ( - maxExtendedSquareWidth = appconsts.DefaultMaxSquareSize * 2 - minExtendedSquareWidth = appconsts.DefaultMinSquareSize * 2 -) - // DataAvailabilityHeader (DAHeader) contains the row and column roots of the // erasure coded version of the data in Block.Data. The original Block.Data is // split into shares and arranged in a square of width squareSize. Then, this @@ -49,16 +44,8 @@ func NewDataAvailabilityHeader(eds *rsmt2d.ExtendedDataSquare) DataAvailabilityH return dah } +// ExtendShares generates an extended data square from given square size and shares func ExtendShares(squareSize uint64, shares [][]byte) (*rsmt2d.ExtendedDataSquare, error) { - // Check that square size is with range - if squareSize < appconsts.DefaultMinSquareSize || squareSize > appconsts.DefaultMaxSquareSize { - return nil, fmt.Errorf( - "invalid square size: min %d max %d provided %d", - appconsts.DefaultMinSquareSize, - appconsts.DefaultMaxSquareSize, - squareSize, - ) - } // check that valid number of shares have been provided if squareSize*squareSize != uint64(len(shares)) { return nil, fmt.Errorf( @@ -115,7 +102,7 @@ func (dah *DataAvailabilityHeader) ToProto() (*daproto.DataAvailabilityHeader, e return dahp, nil } -func DataAvailabilityHeaderFromProto(dahp *daproto.DataAvailabilityHeader) (dah *DataAvailabilityHeader, err error) { +func DataAvailabilityHeaderFromProto(dahp *daproto.DataAvailabilityHeader, minSquareSize, maxSquareSize int) (dah *DataAvailabilityHeader, err error) { if dahp == nil { return nil, errors.New("nil DataAvailabilityHeader") } @@ -124,11 +111,13 @@ func DataAvailabilityHeaderFromProto(dahp *daproto.DataAvailabilityHeader) (dah dah.RowsRoots = dahp.RowRoots dah.ColumnRoots = dahp.ColumnRoots - return dah, dah.ValidateBasic() + return dah, dah.ValidateBasic(minSquareSize, maxSquareSize) } // ValidateBasic runs stateless checks on the DataAvailabilityHeader. -func (dah *DataAvailabilityHeader) ValidateBasic() error { +func (dah *DataAvailabilityHeader) ValidateBasic(minSquareSize, maxSquareSize int) error { + minExtendedSquareWidth := minSquareSize * 2 + maxExtendedSquareWidth := maxSquareSize * 2 if dah == nil { return errors.New("nil data availability header is not valid") } @@ -174,9 +163,9 @@ var tailPaddingShare = append( // MinDataAvailabilityHeader returns the minimum valid data availability header. // It is equal to the data availability header for an empty block -func MinDataAvailabilityHeader() DataAvailabilityHeader { +func MinDataAvailabilityHeader(minSquareSize int) DataAvailabilityHeader { shares := GenerateEmptyShares(appconsts.MinShareCount) - eds, err := ExtendShares(appconsts.DefaultMinSquareSize, shares) + eds, err := ExtendShares(uint64(minSquareSize), shares) if err != nil { panic(err) } diff --git a/pkg/da/data_availability_header_test.go b/pkg/da/data_availability_header_test.go index 08e027c63b..252991cda6 100644 --- a/pkg/da/data_availability_header_test.go +++ b/pkg/da/data_availability_header_test.go @@ -23,13 +23,13 @@ func TestNilDataAvailabilityHeaderHashDoesntCrash(t *testing.T) { } func TestMinDataAvailabilityHeader(t *testing.T) { - dah := MinDataAvailabilityHeader() + dah := MinDataAvailabilityHeader(appconsts.DefaultMinSquareSize) expectedHash := []byte{ 0x6f, 0x52, 0xda, 0xc1, 0x65, 0x45, 0xe4, 0x57, 0x25, 0xbe, 0x6e, 0xa3, 0x2a, 0xed, 0x55, 0x26, 0x6e, 0x45, 0x3, 0x48, 0x0, 0xee, 0xe1, 0xd8, 0x7c, 0x94, 0x28, 0xf4, 0x84, 0x4e, 0xa4, 0x7a, } require.Equal(t, expectedHash, dah.hash) - require.NoError(t, dah.ValidateBasic()) + require.NoError(t, dah.ValidateBasic(appconsts.DefaultMinSquareSize, appconsts.DefaultMaxSquareSize)) // important note: also see the types.TestEmptyBlockDataAvailabilityHeader test // which ensures that empty block data results in the minimum data availability // header @@ -85,12 +85,6 @@ func TestExtendShares(t *testing.T) { } tests := []test{ - { - name: "too large square size", - expectedErr: true, - squareSize: appconsts.DefaultMaxSquareSize + 1, - shares: generateShares((appconsts.DefaultMaxSquareSize+1)*(appconsts.DefaultMaxSquareSize+1), 1), - }, { name: "invalid number of shares", expectedErr: true, @@ -125,7 +119,7 @@ func TestDataAvailabilityHeaderProtoConversion(t *testing.T) { tests := []test{ { name: "min", - dah: MinDataAvailabilityHeader(), + dah: MinDataAvailabilityHeader(appconsts.DefaultMinSquareSize), }, { name: "max", @@ -137,7 +131,7 @@ func TestDataAvailabilityHeaderProtoConversion(t *testing.T) { tt := tt pdah, err := tt.dah.ToProto() require.NoError(t, err) - resDah, err := DataAvailabilityHeaderFromProto(pdah) + resDah, err := DataAvailabilityHeaderFromProto(pdah, appconsts.DefaultMinSquareSize, appconsts.DefaultMaxSquareSize) require.NoError(t, err) resDah.Hash() // calc the hash to make the comparisons fair require.Equal(t, tt.dah, *resDah, tt.name) @@ -170,16 +164,16 @@ func Test_DAHValidateBasic(t *testing.T) { tooSmallDah.ColumnRoots = [][]byte{bytes.Repeat([]byte{2}, 32)} tooSmallDah.RowsRoots = [][]byte{bytes.Repeat([]byte{2}, 32)} // use a bad hash - badHashDah := MinDataAvailabilityHeader() + badHashDah := MinDataAvailabilityHeader(appconsts.DefaultMinSquareSize) badHashDah.hash = []byte{1, 2, 3, 4} // dah with not equal number of roots - mismatchDah := MinDataAvailabilityHeader() + mismatchDah := MinDataAvailabilityHeader(appconsts.DefaultMinSquareSize) mismatchDah.ColumnRoots = append(mismatchDah.ColumnRoots, bytes.Repeat([]byte{2}, 32)) tests := []test{ { name: "min", - dah: MinDataAvailabilityHeader(), + dah: MinDataAvailabilityHeader(appconsts.DefaultMinSquareSize), }, { name: "max", @@ -213,7 +207,7 @@ func Test_DAHValidateBasic(t *testing.T) { for _, tt := range tests { tt := tt - err := tt.dah.ValidateBasic() + err := tt.dah.ValidateBasic(appconsts.DefaultMinSquareSize, appconsts.DefaultMaxSquareSize) if tt.expectErr { require.True(t, strings.Contains(err.Error(), tt.errStr), tt.name) require.Error(t, err) diff --git a/x/blob/keeper/params.go b/x/blob/keeper/params.go index 62ef08be64..64db2c2fd5 100644 --- a/x/blob/keeper/params.go +++ b/x/blob/keeper/params.go @@ -1,6 +1,7 @@ package keeper import ( + "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/x/blob/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -21,12 +22,22 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { // MinSquareSize returns the MinSquareSize param func (k Keeper) MinSquareSize(ctx sdk.Context) (res uint32) { + // use the default size for the first block so that we return a value on the + // first block in PrepareProposal + if ctx.BlockHeader().Height < 1 { + return appconsts.DefaultMinSquareSize + } k.paramStore.Get(ctx, types.KeyMinSquareSize, &res) return } // MaxSquareSize returns the MaxSquareSize param func (k Keeper) MaxSquareSize(ctx sdk.Context) (res uint32) { + // use the default size for the first block so that we return a value on the + // first block in PrepareProposal + if ctx.BlockHeader().Height < 1 { + return appconsts.DefaultMaxSquareSize + } k.paramStore.Get(ctx, types.KeyMaxSquareSize, &res) return } diff --git a/x/blob/types/payforblob.go b/x/blob/types/payforblob.go index b95d090e27..699f6442fd 100644 --- a/x/blob/types/payforblob.go +++ b/x/blob/types/payforblob.go @@ -19,7 +19,6 @@ const ( URLBlobTx = "/blob.BlobTx" URLMsgPayForBlob = "/blob.MsgPayForBlob" ShareSize = appconsts.ShareSize - SquareSize = appconsts.DefaultMaxSquareSize NamespaceIDSize = appconsts.NamespaceSize )