diff --git a/square/builder.go b/square/builder.go index bd701e5..212c012 100644 --- a/square/builder.go +++ b/square/builder.go @@ -16,6 +16,8 @@ import ( type Builder struct { // maxSquareSize is the maximum number of rows (or columns) in the original data square maxSquareSize int + // squareSize imposes an upper bound on the max effective square size. + squareSizeUpperBound int // currentSize is an overestimate for the number of shares used by this builder. currentSize int @@ -32,15 +34,22 @@ type Builder struct { subtreeRootThreshold int } -func NewBuilder(maxSquareSize int, subtreeRootThreshold int, txs ...[]byte) (*Builder, error) { +func NewBuilder(maxSquareSize int, squareSizeUpperBound int, subtreeRootThreshold int, txs ...[]byte) (*Builder, error) { if maxSquareSize <= 0 { return nil, errors.New("max square size must be strictly positive") } if !shares.IsPowerOfTwo(maxSquareSize) { return nil, errors.New("max square size must be a power of two") } + if squareSizeUpperBound <= 0 { + return nil, errors.New("square size upper bound must be strictly positive") + } + if !shares.IsPowerOfTwo(squareSizeUpperBound) { + return nil, errors.New("square size upper bound must be a power of two") + } builder := &Builder{ maxSquareSize: maxSquareSize, + squareSizeUpperBound: squareSizeUpperBound, subtreeRootThreshold: subtreeRootThreshold, Blobs: make([]*Element, 0), Pfbs: make([]*blob.IndexWrapper, 0), @@ -88,7 +97,7 @@ func (b *Builder) AppendBlobTx(blobTx *blob.BlobTx) bool { iw := &blob.IndexWrapper{ Tx: blobTx.Tx, TypeId: blob.ProtoIndexWrapperTypeID, - ShareIndexes: worstCaseShareIndexes(len(blobTx.Blobs), b.maxSquareSize), + ShareIndexes: worstCaseShareIndexes(len(blobTx.Blobs), b.squareSizeUpperBound), } size := proto.Size(iw) pfbShareDiff := b.PfbCounter.Add(size) @@ -407,14 +416,13 @@ func (e Element) maxShareOffset() int { return e.NumShares + e.MaxPadding } -// worstCaseShareIndexes returns the largest possible share indexes for a set -// of blobs at a given appversion. Largest possible is "worst" in that protobuf -// uses varints to encode integers, so larger integers can require more bytes to -// encode. -func worstCaseShareIndexes(blobs, maxSquareSize int) []uint32 { - shareIndexes := make([]uint32, blobs) +// worstCaseShareIndexes returns the largest possible share indexes for a set of +// blobs. Largest possible is "worst" in that protobuf uses varints to encode +// integers, so larger integers can require more bytes to encode. +func worstCaseShareIndexes(blobCount int, squareSizeUpperBound int) []uint32 { + shareIndexes := make([]uint32, blobCount) for i := range shareIndexes { - shareIndexes[i] = uint32(maxSquareSize * maxSquareSize) + shareIndexes[i] = uint32(squareSizeUpperBound * squareSizeUpperBound) } return shareIndexes } diff --git a/square/builder_test.go b/square/builder_test.go index 26a3e36..bb4beb2 100644 --- a/square/builder_test.go +++ b/square/builder_test.go @@ -40,7 +40,7 @@ func TestBuilderSquareSizeEstimation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { txs := generateMixedTxs(tt.normalTxs, tt.pfbCount, 1, tt.pfbSize) - square, _, err := square.Build(txs, 64, defaultSubtreeRootThreshold) + square, _, err := square.Build(txs, 64, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) require.EqualValues(t, tt.expectedSquareSize, square.Size()) }) @@ -48,7 +48,7 @@ func TestBuilderSquareSizeEstimation(t *testing.T) { } func TestBuilderRejectsTransactions(t *testing.T) { - builder, err := square.NewBuilder(2, 64) // 2 x 2 square + builder, err := square.NewBuilder(2, defaultSquareSizeUpperBound, 64) // 2 x 2 square require.NoError(t, err) require.False(t, builder.AppendTx(newTx(shares.AvailableBytesFromCompactShares(4)+1))) require.True(t, builder.AppendTx(newTx(shares.AvailableBytesFromCompactShares(4)))) @@ -81,7 +81,7 @@ func TestBuilderRejectsBlobTransactions(t *testing.T) { for idx, tc := range testCases { t.Run(fmt.Sprintf("case%d", idx), func(t *testing.T) { - builder, err := square.NewBuilder(2, 64) + builder, err := square.NewBuilder(2, defaultSquareSizeUpperBound, 64) require.NoError(t, err) txs := generateBlobTxsWithNamespaces(ns1.Repeat(len(tc.blobSize)), [][]int{tc.blobSize}) require.Len(t, txs, 1) @@ -93,11 +93,11 @@ func TestBuilderRejectsBlobTransactions(t *testing.T) { } func TestBuilderInvalidConstructor(t *testing.T) { - _, err := square.NewBuilder(-4, 64) + _, err := square.NewBuilder(-4, defaultSquareSizeUpperBound, 64) require.Error(t, err) - _, err = square.NewBuilder(0, 64) + _, err = square.NewBuilder(0, defaultSquareSizeUpperBound, 64) require.Error(t, err) - _, err = square.NewBuilder(13, 64) + _, err = square.NewBuilder(13, defaultSquareSizeUpperBound, 64) require.Error(t, err) } @@ -109,7 +109,7 @@ func TestBuilderFindTxShareRange(t *testing.T) { blockTxs := generateOrderedTxs(5, 5, 1000, 10) require.Len(t, blockTxs, 10) - builder, err := square.NewBuilder(128, 64, blockTxs...) + builder, err := square.NewBuilder(128, defaultSquareSizeUpperBound, 64, blockTxs...) require.NoError(t, err) dataSquare, err := builder.Export() @@ -293,7 +293,7 @@ func TestSquareBlobPostions(t *testing.T) { } for i, tt := range tests { t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) { - builder, err := square.NewBuilder(tt.squareSize, defaultSubtreeRootThreshold) + builder, err := square.NewBuilder(tt.squareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) for _, tx := range tt.blobTxs { blobTx, isBlobTx := blob.UnmarshalBlobTx(tx) diff --git a/square/square.go b/square/square.go index 53b0e18..ad63c23 100644 --- a/square/square.go +++ b/square/square.go @@ -15,8 +15,8 @@ import ( // in the square and which have all PFBs trailing regular transactions. Note, this function does // not check the underlying validity of the transactions. // Errors should not occur and would reflect a violation in an invariant. -func Build(txs [][]byte, maxSquareSize, subtreeRootThreshold int) (Square, [][]byte, error) { - builder, err := NewBuilder(maxSquareSize, subtreeRootThreshold) +func Build(txs [][]byte, maxSquareSize, squareSizeUpperBound, subtreeRootThreshold int) (Square, [][]byte, error) { + builder, err := NewBuilder(maxSquareSize, squareSizeUpperBound, subtreeRootThreshold) if err != nil { return nil, nil, err } @@ -44,8 +44,8 @@ func Build(txs [][]byte, maxSquareSize, subtreeRootThreshold int) (Square, [][]b // // Note that this function does not check the underlying validity of // the transactions. -func Construct(txs [][]byte, maxSquareSize, subtreeRootThreshold int) (Square, error) { - builder, err := NewBuilder(maxSquareSize, subtreeRootThreshold, txs...) +func Construct(txs [][]byte, maxSquareSize, squareSizeUpperBound, subtreeRootThreshold int) (Square, error) { + builder, err := NewBuilder(maxSquareSize, squareSizeUpperBound, subtreeRootThreshold, txs...) if err != nil { return nil, err } @@ -144,8 +144,8 @@ func Deconstruct(s Square, decoder PFBDecoder) ([][]byte, error) { // TxShareRange returns the range of share indexes that the tx, specified by txIndex, occupies. // The range is end exclusive. -func TxShareRange(txs [][]byte, txIndex, maxSquareSize, subtreeRootThreshold int) (shares.Range, error) { - builder, err := NewBuilder(maxSquareSize, subtreeRootThreshold, txs...) +func TxShareRange(txs [][]byte, txIndex, maxSquareSize, squareSizeUpperBound, subtreeRootThreshold int) (shares.Range, error) { + builder, err := NewBuilder(maxSquareSize, squareSizeUpperBound, subtreeRootThreshold, txs...) if err != nil { return shares.Range{}, err } @@ -155,8 +155,8 @@ func TxShareRange(txs [][]byte, txIndex, maxSquareSize, subtreeRootThreshold int // BlobShareRange returns the range of share indexes that the blob, identified by txIndex and blobIndex, occupies. // The range is end exclusive. -func BlobShareRange(txs [][]byte, txIndex, blobIndex, maxSquareSize, subtreeRootThreshold int) (shares.Range, error) { - builder, err := NewBuilder(maxSquareSize, subtreeRootThreshold, txs...) +func BlobShareRange(txs [][]byte, txIndex, blobIndex, maxSquareSize, squareSizeUpperBound, subtreeRootThreshold int) (shares.Range, error) { + builder, err := NewBuilder(maxSquareSize, squareSizeUpperBound, subtreeRootThreshold, txs...) if err != nil { return shares.Range{}, err } diff --git a/square/square_benchmark_test.go b/square/square_benchmark_test.go index 55fe41e..c4123cc 100644 --- a/square/square_benchmark_test.go +++ b/square/square_benchmark_test.go @@ -14,7 +14,7 @@ func BenchmarkSquareConstruct(b *testing.B) { txs := generateOrderedTxs(txCount/2, txCount/2, 1, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := square.Construct(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err := square.Construct(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(b, err) } }) @@ -27,7 +27,7 @@ func BenchmarkSquareBuild(b *testing.B) { txs := generateMixedTxs(txCount/2, txCount/2, 1, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, err := square.Build(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, _, err := square.Build(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(b, err) } }) @@ -38,7 +38,7 @@ func BenchmarkSquareBuild(b *testing.B) { txs := generateMixedTxs(0, txCount, 1, blobSize) b.ResetTimer() for i := 0; i < b.N; i++ { - _, _, err := square.Build(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, _, err := square.Build(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(b, err) } }) diff --git a/square/square_test.go b/square/square_test.go index c10a511..ed3d0cf 100644 --- a/square/square_test.go +++ b/square/square_test.go @@ -16,6 +16,7 @@ import ( const ( mebibyte = 1_048_576 // one mebibyte in bytes defaultMaxSquareSize = 128 + defaultSquareSizeUpperBound = 128 defaultSubtreeRootThreshold = 64 ) @@ -25,18 +26,18 @@ func TestSquareConstruction(t *testing.T) { t.Run("normal transactions after PFB transactions", func(t *testing.T) { txs := sendTxs[:5] txs = append(txs, append(pfbTxs, txs...)...) - _, err := square.Construct(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err := square.Construct(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) }) t.Run("not enough space to append transactions", func(t *testing.T) { - _, err := square.Construct(sendTxs, 2, defaultSubtreeRootThreshold) + _, err := square.Construct(sendTxs, 2, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) - _, err = square.Construct(pfbTxs, 2, defaultSubtreeRootThreshold) + _, err = square.Construct(pfbTxs, 2, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) }) t.Run("construction should fail if a single PFB tx contains a blob that is too large to fit in the square", func(t *testing.T) { pfbTxs := test.GenerateBlobTxs(1, 1, 2*mebibyte) - _, err := square.Construct(pfbTxs, 64, defaultSubtreeRootThreshold) + _, err := square.Construct(pfbTxs, 64, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) }) } @@ -100,7 +101,7 @@ func TestSquareTxShareRange(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - shareRange, err := square.TxShareRange(tc.txs, tc.index, 128, 64) + shareRange, err := square.TxShareRange(tc.txs, tc.index, 128, defaultSquareSizeUpperBound, 64) if tc.expectErr { require.Error(t, err) } else { @@ -115,7 +116,7 @@ func TestSquareTxShareRange(t *testing.T) { func TestSquareBlobShareRange(t *testing.T) { txs := test.GenerateBlobTxs(10, 1, 1024) - builder, err := square.NewBuilder(defaultMaxSquareSize, defaultSubtreeRootThreshold, txs...) + builder, err := square.NewBuilder(defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold, txs...) require.NoError(t, err) dataSquare, err := builder.Export() @@ -125,7 +126,7 @@ func TestSquareBlobShareRange(t *testing.T) { blobTx, isBlobTx := blob.UnmarshalBlobTx(tx) require.True(t, isBlobTx) for blobIdx := range blobTx.Blobs { - shareRange, err := square.BlobShareRange(txs, pfbIdx, blobIdx, defaultMaxSquareSize, defaultSubtreeRootThreshold) + shareRange, err := square.BlobShareRange(txs, pfbIdx, blobIdx, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) require.LessOrEqual(t, shareRange.End, len(dataSquare)) blobShares := dataSquare[shareRange.Start:shareRange.End] @@ -136,16 +137,16 @@ func TestSquareBlobShareRange(t *testing.T) { } // error on out of bounds cases - _, err = square.BlobShareRange(txs, -1, 0, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err = square.BlobShareRange(txs, -1, 0, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) - _, err = square.BlobShareRange(txs, 0, -1, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err = square.BlobShareRange(txs, 0, -1, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) - _, err = square.BlobShareRange(txs, 10, 0, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err = square.BlobShareRange(txs, 10, 0, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) - _, err = square.BlobShareRange(txs, 0, 10, defaultMaxSquareSize, defaultSubtreeRootThreshold) + _, err = square.BlobShareRange(txs, 0, 10, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.Error(t, err) } @@ -155,7 +156,7 @@ func TestSquareDeconstruct(t *testing.T) { for _, numTxs := range []int{2, 128, 1024, 8192} { t.Run(fmt.Sprintf("%d", numTxs), func(t *testing.T) { txs := generateOrderedTxs(numTxs/2, numTxs/2, 1, 800) - dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) recomputedTxs, err := square.Deconstruct(dataSquare, test.DecodeMockPFB) require.NoError(t, err) @@ -166,7 +167,7 @@ func TestSquareDeconstruct(t *testing.T) { t.Run("NoPFBs", func(t *testing.T) { const numTxs = 10 txs := test.GenerateTxs(250, 250, numTxs) - dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) recomputedTxs, err := square.Deconstruct(dataSquare, test.DecodeMockPFB) require.NoError(t, err) @@ -174,7 +175,7 @@ func TestSquareDeconstruct(t *testing.T) { }) t.Run("PFBsOnly", func(t *testing.T) { txs := test.GenerateBlobTxs(100, 1, 1024) - dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSubtreeRootThreshold) + dataSquare, err := square.Construct(txs, defaultMaxSquareSize, defaultSquareSizeUpperBound, defaultSubtreeRootThreshold) require.NoError(t, err) recomputedTxs, err := square.Deconstruct(dataSquare, test.DecodeMockPFB) require.NoError(t, err)