Skip to content

Commit df7e080

Browse files
authored
feat: Include Configuration in Fiat-Shamir hash (#7195)
Context: celestiaorg/rsema1d-private#10 This PR adds the configuration (K, N, rowSize) to the seed of the fiat-shamir coefficient derivation. Previously only the rowRoot seeded the fiat-shamir coefficients. Now the coefficients are unique per configuration & rowRoot pair.
1 parent a731799 commit df7e080

14 files changed

Lines changed: 115 additions & 82 deletions

pkg/rsema1d/SPEC.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,28 @@ MapIndexToTreePosition(index, K):
224224
225225
1. **Derive RLC Coefficients**
226226
227+
Bind the row root and the codec parameters (K, N, rowSize) into the
228+
Fiat-Shamir seed so coefficients are unique per (rowRoot, params)
229+
tuple. K, N, and rowSize are each encoded as 4-byte little-endian
230+
unsigned 32-bit integers and absorbed in that order:
231+
227232
```text
228-
seed = SHA256(rowRoot)
229-
numSymbols = rowSize / 2 // Each GF16 symbol is 2 bytes
233+
params = LE32(K) || LE32(N) || LE32(rowSize) // 12 bytes
234+
seed = SHA256(rowRoot || params) // 32 bytes
235+
numSymbols = rowSize / 2 // each GF16 symbol is 2 bytes
230236
for i in 0..numSymbols:
231-
coeffs[i] = HashToGF128(SHA256(seed || i))
237+
coeffs[i] = HashToGF128(SHA256(seed || LE32(i))) // i as 4-byte LE uint32
232238
```
233239

234-
Where HashToGF128 converts a 32-byte hash to a GF128 element by:
240+
Where:
235241

236-
- Taking bytes 0-15 as 8 little-endian uint16 values
237-
- Taking bytes 16-31 as 8 little-endian uint16 values
238-
- XORing corresponding pairs to produce final 8 GF(2^16) values
242+
- `LE32(x)` denotes the 4-byte little-endian encoding of the unsigned
243+
32-bit integer `x`.
244+
- `||` denotes byte concatenation.
245+
- `HashToGF128` converts a 32-byte hash to a GF128 element by:
246+
- Taking bytes 0-15 as 8 little-endian uint16 values
247+
- Taking bytes 16-31 as 8 little-endian uint16 values
248+
- XORing corresponding pairs to produce final 8 GF(2^16) values
239249

240250
1. **Compute RLC Results (Original Rows Only)**
241251

@@ -458,7 +468,7 @@ Row 3: 0x00000000000000000000000000000000000000000000000000000000000000000000000
458468
**Expected commitment**:
459469

460470
```text
461-
0x9f637574ecb67828c5ce7589a0a6ce139ccad3bea8e92d22d9e28fde83a905e7
471+
0xf57fdff87d54f71bc0c860808b046356c8d4850e67b923e08411208df08cb5ab
462472
```
463473

464474
### 6.2 Test Vector 2: K=3, N=9, rowSize=256
@@ -474,7 +484,7 @@ Row 2: 0x00...(255 zero bytes)...03
474484
**Expected commitment**:
475485

476486
```text
477-
0x2d67c13aa6a5c0be41b7e84f36188562c64ff3547ce74ffd410ab1afa7897f22
487+
0x0e0b5f2a0b8e9ef09fbd70256b4c346291450ef239fd98894d177b5f32c579ab
478488
```
479489

480490
### 6.3 Verification Test Cases

pkg/rsema1d/codec.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func Encode(data [][]byte, config *Config) (*ExtendedData, Commitment, []field.G
4040
rowRoot := rowTree.Root()
4141

4242
// 4. Derive RLC coefficients
43-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
43+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
4444

4545
// 5. Compute RLC results for original rows
4646
rlcOrig := computeRLCOrig(data, coeffs, config)
@@ -98,7 +98,7 @@ func EncodeParity(extended [][]byte, config *Config) (*ExtendedData, Commitment,
9898
rowRoot := rowTree.Root()
9999

100100
// 3. Derive RLC coefficients
101-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
101+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
102102

103103
// 4. Compute RLC results for original rows (first K rows)
104104
originalRows := extended[:config.K]

pkg/rsema1d/codec_bench_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ func BenchmarkComputeRLCOrig(b *testing.B) {
220220
}
221221

222222
rowRoot := [32]byte{1, 2, 3, 4}
223-
coeffs := deriveCoefficients(rowRoot, cfg.rowSize)
223+
coeffs := deriveCoefficients(rowRoot, codecConfig.K, codecConfig.N, codecConfig.RowSize)
224224

225225
setup := func() any {
226226
return generateTestData(cfg.k, cfg.rowSize)
@@ -605,7 +605,7 @@ func BenchmarkDeriveCoefficients(b *testing.B) {
605605

606606
b.ResetTimer()
607607
for range b.N {
608-
deriveCoefficients(rowRoot, config.RowSize)
608+
deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
609609
}
610610
})
611611
}

pkg/rsema1d/codec_tamper_test.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestTamperedExtendedDataBeforeCommitment(t *testing.T) {
4141
rowRoot := rowTree.Root()
4242

4343
// Step 3: Derive RLC coefficients
44-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
44+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
4545

4646
// Step 4: Compute RLC results for original rows
4747
rlcOrig := computeRLCOrig(data, coeffs, config)
@@ -129,7 +129,7 @@ func TestTamperedRLCBeforeCommitment(t *testing.T) {
129129
rowRoot := rowTree.Root()
130130

131131
// Step 3: Derive RLC coefficients
132-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
132+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
133133

134134
// Step 4: Compute RLC results for original rows
135135
rlcOrig := computeRLCOrig(data, coeffs, config)
@@ -211,7 +211,7 @@ func TestTamperedOriginalRLCBeforeCommitment(t *testing.T) {
211211
rowRoot := rowTree.Root()
212212

213213
// Step 3: Derive RLC coefficients
214-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
214+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
215215

216216
// Step 4: Compute RLC results for original rows
217217
rlcOrig := computeRLCOrig(data, coeffs, config)
@@ -317,7 +317,7 @@ func TestMultipleTamperedRows(t *testing.T) {
317317
rowTree := buildPaddedRowTree(extended, config)
318318
rowRoot := rowTree.Root()
319319

320-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
320+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
321321
rlcOrig := computeRLCOrig(data, coeffs, config)
322322

323323
// Build padded RLC Merkle tree
@@ -403,11 +403,8 @@ func TestInvalidRowProofDepth(t *testing.T) {
403403
rowTree := buildPaddedRowTree(extended, config)
404404
rowRoot := rowTree.Root()
405405

406-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
406+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
407407
rlcOrig := computeRLCOrig(data, coeffs, config)
408-
if err != nil {
409-
t.Fatalf("ExtendRLCResults failed: %v", err)
410-
}
411408

412409
rlcOrigTree := BuildPaddedRLCTree(rlcOrig, config)
413410
rlcOrigRoot := rlcOrigTree.Root()
@@ -449,7 +446,7 @@ func TestInvalidRowProofDepth(t *testing.T) {
449446
if err != nil {
450447
t.Errorf("Failed to compute fake row root: %v", err)
451448
}
452-
fakeCoeffs := deriveCoefficients(fakeRowRoot, config.RowSize)
449+
fakeCoeffs := deriveCoefficients(fakeRowRoot, config.K, config.N, config.RowSize)
453450
fakeRlcCommitment := computeRLC(maliciousProof.Row, fakeCoeffs)
454451
ctx.rlcOrigRoot = rlcOrigRoot
455452

@@ -494,7 +491,7 @@ func TestVerifyRowWithContextWithMultipleOpenings(t *testing.T) {
494491
assert.NoError(t, err)
495492
nodes, asNodes := buildAdversarialPaddedRowTree(extended)
496493
rowRoot := nodes[0]
497-
coeffs := deriveCoefficients([32]byte(rowRoot), config.RowSize)
494+
coeffs := deriveCoefficients([32]byte(rowRoot), config.K, config.N, config.RowSize)
498495
rlcOrig := computeRLCOrig(data, coeffs, config)
499496
rlcOrigTree := BuildPaddedRLCTree(rlcOrig, config)
500497
rlcOrigRoot := rlcOrigTree.Root()

pkg/rsema1d/codec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ func TestRLCCommutationProperty(t *testing.T) {
269269
t.Fatalf("Encode() error: %v", err)
270270
}
271271

272-
coeffs := deriveCoefficients(extData.rowRoot, config.RowSize)
272+
coeffs := deriveCoefficients(extData.rowRoot, config.K, config.N, config.RowSize)
273273
extendedRLCs, err := encoding.ExtendRLCResults(extData.rlcOrig, tc.n)
274274
if err != nil {
275275
t.Fatalf("ExtendRLCResults() error: %v", err)

pkg/rsema1d/coder.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ func (c *Coder) commit(extendedRows [][]byte) *ExtendedData {
6565
rowTree := buildPaddedRowTree(extendedRows, c.config)
6666
rowRoot := rowTree.Root()
6767

68-
// derive RLC coefficients and compute RLC results for original rows
69-
coeffs := deriveCoefficients(rowRoot, len(extendedRows[0]))
68+
// derive RLC coefficients and compute RLC results for original rows.
69+
// rowSize is taken from the data to support the Coder's deferred-RowSize
70+
// mode (config.RowSize may be 0 when the Coder is reused across shards).
71+
coeffs := deriveCoefficients(rowRoot, c.config.K, c.config.N, len(extendedRows[0]))
7072
rlcOrig := computeRLCVectorized(extendedRows[:c.config.K], coeffs, c.config)
7173

7274
// build padded RLC Merkle tree

pkg/rsema1d/commitment.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,29 @@ import (
99
"github.com/celestiaorg/celestia-app/v9/pkg/rsema1d/field"
1010
)
1111

12-
// deriveCoefficients generates RLC coefficients via Fiat-Shamir, fanning
13-
// the SHA256 loop out across GOMAXPROCS via static chunking above the
14-
// parallel break-even. Below the threshold goroutine startup would dwarf
15-
// the per-iteration SHA256.
16-
func deriveCoefficients(rowRoot [32]byte, rowSize int) []field.GF128 {
17-
numSymbols := rowSize / 2
12+
// deriveCoefficients generates RLC coefficients via Fiat-Shamir (internal).
13+
// k, n, and rowSize are bound into the seed so coefficients are unique per
14+
// (rowRoot, k, n, rowSize) tuple. Callers pick where rowSize comes from:
15+
// producer paths (Encode) pass config.RowSize; consumer paths (Verify, Coder)
16+
// pass the actual data length.
17+
func deriveCoefficients(rowRoot [32]byte, k, n, rowSize int) []field.GF128 {
18+
// Bind rowRoot and the codec parameters into the Fiat-Shamir seed so
19+
// coefficients are unique per (rowRoot, k, n, rowSize) tuple.
20+
h := sha256.New()
21+
h.Write(rowRoot[:])
22+
var params [12]byte
23+
binary.LittleEndian.PutUint32(params[0:4], uint32(k))
24+
binary.LittleEndian.PutUint32(params[4:8], uint32(n))
25+
binary.LittleEndian.PutUint32(params[8:12], uint32(rowSize))
26+
h.Write(params[:])
27+
var seed [32]byte
28+
h.Sum(seed[:0])
29+
30+
numSymbols := rowSize / 2 // Each GF16 symbol is 2 bytes
1831
coeffs := make([]field.GF128, numSymbols)
1932
workers := min(runtime.GOMAXPROCS(0), numSymbols)
2033
if workers <= 1 || numSymbols < minParallelDeriveSymbols {
21-
deriveCoefficientsRange(rowRoot, coeffs, 0, numSymbols)
34+
deriveCoefficientsRange(seed, coeffs, 0, numSymbols)
2235
return coeffs
2336
}
2437
chunk := (numSymbols + workers - 1) / workers
@@ -29,7 +42,7 @@ func deriveCoefficients(rowRoot [32]byte, rowSize int) []field.GF128 {
2942
end := min(start+chunk, numSymbols)
3043
go func(start, end int) {
3144
defer wg.Done()
32-
deriveCoefficientsRange(rowRoot, coeffs, start, end)
45+
deriveCoefficientsRange(seed, coeffs, start, end)
3346
}(start, end)
3447
}
3548
wg.Wait()
@@ -40,10 +53,13 @@ func deriveCoefficients(rowRoot [32]byte, rowSize int) []field.GF128 {
4053
// parallel is slower below ~256 and 1.3-7× faster from 512 upward.
4154
const minParallelDeriveSymbols = 512
4255

43-
func deriveCoefficientsRange(rowRoot [32]byte, coeffs []field.GF128, start, end int) {
44-
seed := sha256.Sum256(rowRoot[:])
56+
func deriveCoefficientsRange(seed [32]byte, coeffs []field.GF128, start, end int) {
4557
var input [32 + 4]byte
4658
copy(input[:32], seed[:])
59+
60+
// Reuse a single SHA256 hasher with Reset() between iterations.
61+
// This avoids re-initializing the digest state from scratch on each call
62+
// to sha256.Sum256, saving ~12% on coefficient derivation.
4763
h := sha256.New()
4864
var digest [32]byte
4965
for i := start; i < end; i++ {

pkg/rsema1d/commitment_test.go

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ func TestDeriveCoefficients(t *testing.T) {
3333
rowRoot := sha256.Sum256([]byte("test root"))
3434

3535
// Derive coefficients
36-
coeffs1 := deriveCoefficients(rowRoot, config.RowSize)
37-
coeffs2 := deriveCoefficients(rowRoot, config.RowSize)
36+
coeffs1 := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
37+
coeffs2 := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
3838

3939
// Test determinism
4040
if len(coeffs1) != len(coeffs2) {
@@ -55,7 +55,7 @@ func TestDeriveCoefficients(t *testing.T) {
5555

5656
// Test that different roots produce different coefficients
5757
differentRoot := sha256.Sum256([]byte("different root"))
58-
coeffs3 := deriveCoefficients(differentRoot, config.RowSize)
58+
coeffs3 := deriveCoefficients(differentRoot, config.K, config.N, config.RowSize)
5959

6060
allSame := true
6161
for i := range coeffs1 {
@@ -98,7 +98,7 @@ func TestComputeRLC(t *testing.T) {
9898

9999
// Derive coefficients
100100
rowRoot := sha256.Sum256([]byte("test"))
101-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
101+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
102102

103103
// Compute RLC
104104
rlc1 := computeRLC(row, coeffs)
@@ -190,7 +190,7 @@ func TestRLCLinearity(t *testing.T) {
190190

191191
// Derive coefficients
192192
rowRoot := sha256.Sum256([]byte("test"))
193-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
193+
coeffs := deriveCoefficients(rowRoot, config.K, config.N, config.RowSize)
194194

195195
// Compute RLCs
196196
rlcA := computeRLC(rowA, coeffs)
@@ -247,45 +247,50 @@ func TestCommitmentDeterminism(t *testing.T) {
247247
}
248248

249249
func TestCoefficientsConsistency(t *testing.T) {
250-
// Test that coefficient derivation is consistent across different row counts
251-
// but same rowSize
252-
configs := []struct {
253-
k int
254-
n int
255-
}{
256-
{4, 4},
257-
{8, 8},
258-
{16, 16},
259-
}
260-
261-
rowSize := 128
250+
// Coefficient derivation must be deterministic for a given
251+
// (rowRoot, K, N, RowSize) tuple, and must differ when any of those
252+
// transcript inputs change.
262253
rowRoot := sha256.Sum256([]byte("consistent root"))
254+
base := &Config{K: 8, N: 8, RowSize: 128, WorkerCount: 1}
263255

264-
var prevCoeffs []field.GF128
256+
baseCoeffs := deriveCoefficients(rowRoot, base.K, base.N, base.RowSize)
265257

266-
for i, tc := range configs {
267-
config := &Config{
268-
K: tc.k,
269-
N: tc.n,
270-
RowSize: rowSize,
271-
WorkerCount: 1,
258+
// Same inputs → identical output.
259+
again := deriveCoefficients(rowRoot, base.K, base.N, base.RowSize)
260+
if len(again) != len(baseCoeffs) {
261+
t.Fatalf("deterministic: length mismatch: got %d want %d", len(again), len(baseCoeffs))
262+
}
263+
for i := range baseCoeffs {
264+
if !field.Equal128(baseCoeffs[i], again[i]) {
265+
t.Fatalf("deterministic: coefficient %d differs", i)
272266
}
267+
}
273268

274-
coeffs := deriveCoefficients(rowRoot, config.RowSize)
275-
276-
// All configs with same rowSize should produce same coefficients
277-
if i > 0 {
278-
if len(coeffs) != len(prevCoeffs) {
279-
t.Fatalf("Config %d: coefficient count differs", i)
280-
}
281-
282-
for j := range coeffs {
283-
if !field.Equal128(coeffs[j], prevCoeffs[j]) {
284-
t.Errorf("Config %d: coefficient %d differs", i, j)
285-
}
286-
}
269+
// Varying any of K, N, RowSize must change the coefficients.
270+
variants := []struct {
271+
name string
272+
config *Config
273+
}{
274+
{"different K", &Config{K: 4, N: 8, RowSize: 128, WorkerCount: 1}},
275+
{"different N", &Config{K: 8, N: 16, RowSize: 128, WorkerCount: 1}},
276+
{"different RowSize", &Config{K: 8, N: 8, RowSize: 256, WorkerCount: 1}},
277+
}
278+
for _, v := range variants {
279+
got := deriveCoefficients(rowRoot, v.config.K, v.config.N, v.config.RowSize)
280+
if equalGF128Slice(got, baseCoeffs) {
281+
t.Errorf("%s: coefficients unexpectedly equal to base", v.name)
287282
}
283+
}
284+
}
288285

289-
prevCoeffs = coeffs
286+
func equalGF128Slice(a, b []field.GF128) bool {
287+
if len(a) != len(b) {
288+
return false
289+
}
290+
for i := range a {
291+
if !field.Equal128(a[i], b[i]) {
292+
return false
293+
}
290294
}
295+
return true
291296
}

pkg/rsema1d/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ func (c *Config) Validate() error {
3939
if c.N <= 0 {
4040
return errors.New("n must be positive")
4141
}
42-
// RowSize=0 is valid (means variable row size, determined at runtime)
42+
// RowSize=0 is valid (means variable row size, determined at runtime by
43+
// the Coder, which derives it from the data passed to each operation).
4344
if c.RowSize < 0 {
4445
return errors.New("RowSize must be non-negative")
4546
}

pkg/rsema1d/incorrect_encoding.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func GenerateIncorrectEncoding(data [][]byte, config *Config, rowIndices []int,
5959
newRowRoot := newRowTree.Root()
6060

6161
// 4. Derive new coefficients from the new row root
62-
newCoeffs := deriveCoefficients(newRowRoot, config.RowSize)
62+
newCoeffs := deriveCoefficients(newRowRoot, config.K, config.N, config.RowSize)
6363

6464
// 5. Compute RLC using non-tampered original rows: for any tampered original
6565
// row, use the saved pre-tamper data so the RLC values stay correct for them.

0 commit comments

Comments
 (0)