diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/csp.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/csp.go index 13b4f50a91..2ffa4a1a7d 100644 --- a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/csp.go +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/csp.go @@ -12,6 +12,38 @@ import ( "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" ) +// collapseTrailingEqualPoints folds a maximal trailing run of identical points +// into a single entry whose scalar is the sum of the run's scalars. As MSM is +// linear, MSM(points, scalars) is unchanged. +// +// The CSP generators are power-of-two padded with a trailing run of GenG1, so +// this shrinks the gen_f MSM to its distinct generators. It returns reduced +// views of the inputs and overwrites scalars[start] with the run sum, so the +// caller must not reuse scalars afterwards. The slices are returned unchanged +// when the trailing run has length < 2. +func collapseTrailingEqualPoints(points []*mathlib.G1, scalars []*mathlib.Zr, curve *mathlib.Curve) ([]*mathlib.G1, []*mathlib.Zr) { + n := len(points) + if n < 2 { + return points, scalars + } + + start := n - 1 + for start > 0 && points[start-1].Equals(points[n-1]) { + start-- + } + if start == n-1 { + return points, scalars // no run to fold + } + + sum := scalars[start].Copy() + for i := start + 1; i < n; i++ { + sum = curve.ModAdd(sum, scalars[i], curve.GroupOrder) + } + scalars[start] = sum + + return points[:start+1], scalars[:start+1] +} + // Proof is the non-interactive compressed sigma-protocol proof for a linear // form evaluation over a Pedersen commitment. // @@ -295,25 +327,39 @@ func (v *verifier) Verify(proof *Proof) error { comScalars = append(comScalars, mulScratch.Copy()) } - com := v.Curve.MultiScalarMul(comPoints, comScalars) - // Compute the coefficient vector s such that // gen_f = sum_i s[i] · gen[i] and f_f = sum_i s[i] · f[i] // then evaluate both via a single MSM and a single inner product. n := 1 << v.NumberOfRounds s := sVector(n, challenges, v.Curve) - // gen_f = MSM(Generators, s) - genF := v.Curve.MultiScalarMul(v.Generators, s) - - // f_f = ⟨s, LinearForm⟩ (scalar-field MSM via ModAddMul) + // f_f = ⟨s, LinearForm⟩ (scalar-field MSM via ModAddMul). + // Computed before the gen_f collapse below, which may truncate s in place. fF := math.InnerProduct(s, v.LinearForm, v.Curve) - // Final check: com_f^{f_f} == gen_f^{val_f} - lhs := com.Mul(fF) - rhs := genF.Mul(val) + // gen_f = MSM(Generators, s). The generators are power-of-two padded with a + // trailing run of GenG1; fold it away so the MSM runs over distinct points. + genPoints, genScalars := collapseTrailingEqualPoints(v.Generators, s, v.Curve) + + // Final check: com^{f_f} == gen_f^{val}, where com = MSM(comPoints, comScalars) + // and gen_f = MSM(genPoints, genScalars). Since MSM is linear this is the same as + // MSM(comPoints ∪ genPoints, [comScalars·f_f, genScalars·(−val)]) == identity, + // so we pre-scale the two scalar vectors (field muls), concatenate, and run a + // single MSM instead of two MSMs plus two EC scalar multiplications. + negVal := v.Curve.ModNeg(val, v.Curve.GroupOrder) + + allPoints := make([]*mathlib.G1, 0, len(comPoints)+len(genPoints)) + allScalars := make([]*mathlib.Zr, 0, len(comPoints)+len(genPoints)) + for i := range comPoints { + allPoints = append(allPoints, comPoints[i]) + allScalars = append(allScalars, v.Curve.ModMul(comScalars[i], fF, v.Curve.GroupOrder)) + } + for i := range genPoints { + allPoints = append(allPoints, genPoints[i]) + allScalars = append(allScalars, v.Curve.ModMul(genScalars[i], negVal, v.Curve.GroupOrder)) + } - if !lhs.Equals(rhs) { + if !v.Curve.MultiScalarMul(allPoints, allScalars).IsInfinity() { return errors.New("CSP proof verification failed") } diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_arithmetic_test.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_arithmetic_test.go new file mode 100644 index 0000000000..f07b85c03f --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_arithmetic_test.go @@ -0,0 +1,133 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package csp + +import ( + "testing" + + math "github.com/IBM/mathlib" + bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/stretchr/testify/require" +) + +// TestFieldArithmeticOptimization verifies that the polynomial expansion +// (c-1)*(c-2) = c² - 3c + 2 holds in field arithmetic. +func TestFieldArithmeticOptimization(t *testing.T) { + curve := math.Curves[math.BN254] + rand, err := curve.Rand() + require.NoError(t, err) + + // Generate a random field element c + cZr := curve.NewRandomZr(rand) + var c bn254fr.Element + c.SetBigInt(cZr.BigInt()) + + // Compute (c-1) + var cMinus1 bn254fr.Element + var one bn254fr.Element + one.SetOne() + cMinus1.Sub(&c, &one) + + // Compute (c-2) + var cMinus2 bn254fr.Element + var two bn254fr.Element + two.SetInt64(2) + cMinus2.Sub(&c, &two) + + // Direct multiplication: (c-1)*(c-2) + var directProduct bn254fr.Element + directProduct.Mul(&cMinus1, &cMinus2) + + // Optimized computation: c² - 3c + 2 + var cSquared bn254fr.Element + cSquared.Mul(&c, &c) + + var three bn254fr.Element + three.SetInt64(3) + + var threeC bn254fr.Element + threeC.Mul(&three, &c) + + var optimized bn254fr.Element + optimized.Sub(&cSquared, &threeC) + optimized.Add(&optimized, &two) + + // Verify they are equal + require.Equal(t, directProduct.Bytes(), optimized.Bytes(), + "(c-1)*(c-2) should equal c² - 3c + 2 in field arithmetic") + + t.Logf("✓ Verified: (c-1)*(c-2) = c² - 3c + 2") + t.Logf(" Direct product: %x", directProduct.Bytes()) + t.Logf(" Optimized: %x", optimized.Bytes()) +} + +// TestFieldArithmeticOptimizationPattern verifies the general pattern: +// (c-i)*(c-j) = c² - c*(i+j) + i*j +func TestFieldArithmeticOptimizationPattern(t *testing.T) { + curve := math.Curves[math.BN254] + rand, err := curve.Rand() + require.NoError(t, err) + + // Generate a random field element c + cZr := curve.NewRandomZr(rand) + var c bn254fr.Element + c.SetBigInt(cZr.BigInt()) + + // Test several (i, j) pairs + testCases := []struct { + i, j int64 + }{ + {0, 3}, // (c-0)*(c-3) = c² - 3c + {1, 2}, // (c-1)*(c-2) = c² - 3c + 2 + {0, 7}, // (c-0)*(c-7) = c² - 7c + {2, 5}, // (c-2)*(c-5) = c² - 7c + 10 + } + + for _, tc := range testCases { + // Compute (c-i) + var cMinusI bn254fr.Element + var iElem bn254fr.Element + iElem.SetInt64(tc.i) + cMinusI.Sub(&c, &iElem) + + // Compute (c-j) + var cMinusJ bn254fr.Element + var jElem bn254fr.Element + jElem.SetInt64(tc.j) + cMinusJ.Sub(&c, &jElem) + + // Direct multiplication: (c-i)*(c-j) + var directProduct bn254fr.Element + directProduct.Mul(&cMinusI, &cMinusJ) + + // Optimized computation: c² - c*(i+j) + i*j + var cSquared bn254fr.Element + cSquared.Mul(&c, &c) + + var iPlusJ bn254fr.Element + iPlusJ.SetInt64(tc.i + tc.j) + + var cTimesSumIJ bn254fr.Element + cTimesSumIJ.Mul(&c, &iPlusJ) + + var iTimesJ bn254fr.Element + iTimesJ.SetInt64(tc.i * tc.j) + + var optimized bn254fr.Element + optimized.Sub(&cSquared, &cTimesSumIJ) + optimized.Add(&optimized, &iTimesJ) + + // Verify they are equal + require.Equal(t, directProduct.Bytes(), optimized.Bytes(), + "(c-%d)*(c-%d) should equal c² - c*%d + %d in field arithmetic", + tc.i, tc.j, tc.i+tc.j, tc.i*tc.j) + + t.Logf("✓ Verified: (c-%d)*(c-%d) = c² - c*%d + %d", tc.i, tc.j, tc.i+tc.j, tc.i*tc.j) + } +} + +// Made with Bob diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go new file mode 100644 index 0000000000..5b50b77fa0 --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go @@ -0,0 +1,428 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package csp + +import ( + "testing" + + mathlib "github.com/IBM/mathlib" + bls12381fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// naiveNumeratorsBLS computes ∏_{j≠i} input[j] for each i using a straightforward +// O(n²) loop. Used as a reference oracle in tests. +func naiveNumeratorsBLS(input []*bls12381fr.Element, m int) []*bls12381fr.Element { + out := make([]*bls12381fr.Element, m) + for i := range m { + out[i] = new(bls12381fr.Element).SetOne() + for j := range m { + if j != i { + out[i].Mul(out[i], input[j]) + } + } + } + + return out +} + +// naiveNumeratorsBN254 is the BN254 counterpart of naiveNumeratorsBLS. +func naiveNumeratorsBN254(input []*bn254fr.Element, m int) []*bn254fr.Element { + out := make([]*bn254fr.Element, m) + for i := range m { + out[i] = new(bn254fr.Element).SetOne() + for j := range m { + if j != i { + out[i].Mul(out[i], input[j]) + } + } + } + + return out +} + +// blsNumeratorsBinaryTree runs the binary tree algorithm for the given c and m, +// and returns the numerators. Test-only helper. +func blsNumeratorsBinaryTree(c int64, m int) []*bls12381fr.Element { + curve := mathlib.Curves[mathlib.BLS12_381] + cZr := curve.NewZrFromInt(c) + pooled := getTreeArrays[bls12381fr.Element](m) + result := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](m, cZr, pooled) + putTreeArrays(pooled) + + return result +} + +// bn254NumeratorsBinaryTree is the BN254 counterpart of blsNumeratorsBinaryTree. +func bn254NumeratorsBinaryTree(c int64, m int) []*bn254fr.Element { + curve := mathlib.Curves[mathlib.BN254] + cZr := curve.NewZrFromInt(c) + pooled := getTreeArrays[bn254fr.Element](m) + result := computeNumeratorsBinaryTree[bn254fr.Element, *bn254fr.Element](m, cZr, pooled) + putTreeArrays(pooled) + + return result +} + +// buildCMinusJ builds a cMinusJE slice for a given integer c over m elements: +// cMinusJE[j] = c - j for j = 0..m-1. +func buildCMinusJBLS(c int64, m int) []*bls12381fr.Element { + elems := make([]*bls12381fr.Element, m) + for j := range m { + e := new(bls12381fr.Element) + e.SetInt64(c - int64(j)) // #nosec G115 + elems[j] = e + } + + return elems +} + +func buildCMinusJBN254(c int64, m int) []*bn254fr.Element { + elems := make([]*bn254fr.Element, m) + for j := range m { + e := new(bn254fr.Element) + e.SetInt64(c - int64(j)) // #nosec G115 + elems[j] = e + } + + return elems +} + +// equalsBLS reports whether two BLS12-381 elements are equal. +func equalsBLS(a, b *bls12381fr.Element) bool { return a.Equal(b) } + +// equalsBN254 reports whether two BN254 elements are equal. +func equalsBN254(a, b *bn254fr.Element) bool { return a.Equal(b) } + +// TestComputeNumeratorsBinaryTreeVsOriginalBLS verifies that the binary-tree +// implementation produces the same numerators as a naive O(n²) reference for +// the BLS12-381 field, across a range of m values that exercise different tree +// shapes (perfect and imperfect binary trees). +// +// Note: m=1 is not a valid input for computeNumeratorsBinaryTree (callers +// always pass m = n+1 with n ≥ 1, so m ≥ 2). The m=1 case is therefore +// excluded from this table. +// +// Given cMinusJE[j] = c-j for various (c, m) pairs, +// When computeNumeratorsBinaryTree is called, +// Then each numers[i] must equal ∏_{j≠i} cMinusJE[j]. +func TestComputeNumeratorsBinaryTreeVsOriginalBLS(t *testing.T) { + // m values: 2 (root-is-parent-of-two-leaves), 3 (first non-power-of-2), + // 4 (perfect), 5 and 7 (imperfect), 8 (perfect), 13 (imperfect). + mValues := []int{2, 3, 4, 5, 7, 8, 13} + cValues := []int64{100, 7, 42} + + for _, m := range mValues { + for _, c := range cValues { + t.Run("", func(t *testing.T) { + t.Logf("m=%d c=%d", m, c) + input := buildCMinusJBLS(c, m) + + got := blsNumeratorsBinaryTree(c, m) + want := naiveNumeratorsBLS(input, m) + + require.Len(t, got, m, "result length must equal m") + for i := range m { + assert.True(t, equalsBLS(got[i], want[i]), + "m=%d c=%d i=%d: binary-tree result differs from original", m, c, i) + } + }) + } + } +} + +// TestComputeNumeratorsBinaryTreeVsOriginalBN254 verifies the binary-tree +// implementation against the naive reference for BN254. +func TestComputeNumeratorsBinaryTreeVsOriginalBN254(t *testing.T) { + mValues := []int{2, 3, 4, 5, 7, 8, 13} + cValues := []int64{100, 7, 42} + + for _, m := range mValues { + for _, c := range cValues { + t.Run("", func(t *testing.T) { + t.Logf("m=%d c=%d", m, c) + input := buildCMinusJBN254(c, m) + + got := bn254NumeratorsBinaryTree(c, m) + want := naiveNumeratorsBN254(input, m) + + require.Len(t, got, m, "result length must equal m") + for i := range m { + assert.True(t, equalsBN254(got[i], want[i]), + "m=%d c=%d i=%d: binary-tree result differs from original", m, c, i) + } + }) + } + } +} + +// TestComputeNumeratorsBinaryTreeTwoElements verifies the m=2 case. +// +// With c=10: +// - cMinusJ[0] = 10, cMinusJ[1] = 9 +// - numers[0] = cMinusJ[1] = 9 +// - numers[1] = cMinusJ[0] = 10 +func TestComputeNumeratorsBinaryTreeTwoElements(t *testing.T) { + // c=10, m=2 + got := blsNumeratorsBinaryTree(10, 2) + require.Len(t, got, 2) + + var nine, ten bls12381fr.Element + nine.SetInt64(9) + ten.SetInt64(10) + + assert.True(t, equalsBLS(got[0], &nine), "m=2: numers[0] should be c-1 = 9") + assert.True(t, equalsBLS(got[1], &ten), "m=2: numers[1] should be c-0 = 10") +} + +// TestComputeNumeratorsBinaryTreeThreeElements verifies the m=3 case with +// hand-computed expected values. +// +// With c=10: +// - cMinusJ = [10, 9, 8] +// - numers[0] = 9*8 = 72 +// - numers[1] = 10*8 = 80 +// - numers[2] = 10*9 = 90 +func TestComputeNumeratorsBinaryTreeThreeElements(t *testing.T) { + got := blsNumeratorsBinaryTree(10, 3) + require.Len(t, got, 3) + + expected := []int64{72, 80, 90} + for i, exp := range expected { + var e bls12381fr.Element + e.SetInt64(exp) + assert.True(t, equalsBLS(got[i], &e), "m=3: numers[%d] should be %d", i, exp) + } +} + +// TestComputeNumeratorsBinaryTreeFourElements verifies the m=4 perfect binary tree. +// +// With c=10: +// - cMinusJ = [10, 9, 8, 7] +// - numers[0] = 9*8*7 = 504 +// - numers[1] = 10*8*7 = 560 +// - numers[2] = 10*9*7 = 630 +// - numers[3] = 10*9*8 = 720 +func TestComputeNumeratorsBinaryTreeFourElements(t *testing.T) { + got := blsNumeratorsBinaryTree(10, 4) + require.Len(t, got, 4) + + expected := []int64{504, 560, 630, 720} + for i, exp := range expected { + var e bls12381fr.Element + e.SetInt64(exp) + assert.True(t, equalsBLS(got[i], &e), "m=4: numers[%d] should be %d", i, exp) + } +} + +// TestComputeNumeratorsBinaryTreeOutputLength checks that the returned slice +// always has exactly m elements for various m values. +// m=1 is excluded (not a valid input; callers always pass m ≥ 2). +func TestComputeNumeratorsBinaryTreeOutputLength(t *testing.T) { + for _, m := range []int{2, 3, 4, 5, 6, 7, 8, 15, 16, 17} { + got := blsNumeratorsBinaryTree(1000, m) + assert.Len(t, got, m, "expected output length == m for m=%d", m) + } +} + +// TestComputeNumeratorsBinaryTreeAllDifferent verifies that distinct input +// elements produce distinct numerators (no accidental aliasing in the tree). +// +// For c far from 0..m-1, all (c-j) are distinct non-zero integers, so the +// products ∏_{j≠i} (c-j) are also distinct. +func TestComputeNumeratorsBinaryTreeAllDifferent(t *testing.T) { + m := 5 + c := int64(1000) // far from 0..4 so all values are distinct + + got := blsNumeratorsBinaryTree(c, m) + require.Len(t, got, m) + + // Verify no two numerators are equal + for i := range m { + for j := i + 1; j < m; j++ { + assert.False(t, equalsBLS(got[i], got[j]), + "m=%d c=%d: numers[%d] and numers[%d] should be distinct", m, c, i, j) + } + } +} + +// TestComputeNumeratorsBinaryTreeDoesNotMutateInput verifies that the input +// cMinusJE slice is not modified by the algorithm (no aliasing between input +// storage and the tree's internal scratch space). +func TestComputeNumeratorsBinaryTreeDoesNotMutateInput(t *testing.T) { + m := 6 + c := int64(50) + + // Run twice with the same c; both calls must return identical results + // (the pooled slab is not carried over between calls). + got1 := blsNumeratorsBinaryTree(c, m) + got2 := blsNumeratorsBinaryTree(c, m) + + for i := range m { + assert.True(t, equalsBLS(got1[i], got2[i]), + "repeated call with same c=%d produced different result at index %d", c, i) + } +} + +// TestComputeNumeratorsBinaryTreeLargePerfectTree stress-tests a large perfect +// binary tree (m=32) against the original implementation for both curves. +func TestComputeNumeratorsBinaryTreeLargePerfectTree(t *testing.T) { + m := 32 + c := int64(10000) + + t.Run("BLS12381", func(t *testing.T) { + input := buildCMinusJBLS(c, m) + got := blsNumeratorsBinaryTree(c, m) + want := naiveNumeratorsBLS(input, m) + require.Len(t, got, m) + for i := range m { + assert.True(t, equalsBLS(got[i], want[i]), "m=%d i=%d: mismatch", m, i) + } + }) + + t.Run("BN254", func(t *testing.T) { + input := buildCMinusJBN254(c, m) + got := bn254NumeratorsBinaryTree(c, m) + want := naiveNumeratorsBN254(input, m) + require.Len(t, got, m) + for i := range m { + assert.True(t, equalsBN254(got[i], want[i]), "m=%d i=%d: mismatch", m, i) + } + }) +} + +// TestComputeNumeratorsBinaryTreeLargeImperfectTree stress-tests an imperfect +// binary tree (m=33) against the original implementation. +func TestComputeNumeratorsBinaryTreeLargeImperfectTree(t *testing.T) { + m := 33 + c := int64(10000) + + input := buildCMinusJBLS(c, m) + got := blsNumeratorsBinaryTree(c, m) + want := naiveNumeratorsBLS(input, m) + + require.Len(t, got, m) + for i := range m { + assert.True(t, equalsBLS(got[i], want[i]), "m=%d i=%d: mismatch", m, i) + } +} + +// TestComputeNumeratorsBinaryTreeEightElements verifies the m=8 case +// (a perfect binary tree of depth 3) with hand-computed expected values. +// +// With c=10: +// - cMinusJ = [10, 9, 8, 7, 6, 5, 4, 3] +// - numers[0] = 9×8×7×6×5×4×3 = 181440 +// - numers[1] = 10×8×7×6×5×4×3 = 201600 +// - numers[2] = 10×9×7×6×5×4×3 = 226800 +// - numers[3] = 10×9×8×6×5×4×3 = 259200 +// - numers[4] = 10×9×8×7×5×4×3 = 302400 +// - numers[5] = 10×9×8×7×6×4×3 = 362880 +// - numers[6] = 10×9×8×7×6×5×3 = 453600 +// - numers[7] = 10×9×8×7×6×5×4 = 604800 +func TestComputeNumeratorsBinaryTreeEightElements(t *testing.T) { + got := blsNumeratorsBinaryTree(10, 8) + require.Len(t, got, 8) + + expected := []int64{181440, 201600, 226800, 259200, 302400, 362880, 453600, 604800} + for i, exp := range expected { + var e bls12381fr.Element + e.SetInt64(exp) + assert.True(t, equalsBLS(got[i], &e), "m=8: numers[%d] should be %d", i, exp) + } + + // Also verify against naive reference for both curves. + t.Run("BLS12381", func(t *testing.T) { + input := buildCMinusJBLS(10, 8) + want := naiveNumeratorsBLS(input, 8) + for i := range 8 { + assert.True(t, equalsBLS(got[i], want[i]), "m=8 BLS: index %d mismatch", i) + } + }) + + t.Run("BN254", func(t *testing.T) { + input := buildCMinusJBN254(10, 8) + got8 := bn254NumeratorsBinaryTree(10, 8) + want := naiveNumeratorsBN254(input, 8) + require.Len(t, got8, 8) + for i := range 8 { + assert.True(t, equalsBN254(got8[i], want[i]), "m=8 BN254: index %d mismatch", i) + } + }) +} + +// TestComputeNumeratorsBinaryTreeTenElements verifies the m=10 case +// (an even, non-power-of-2 tree) with hand-computed expected values. +// +// With c=100: +// - cMinusJ = [100, 99, 98, 97, 96, 95, 94, 93, 92, 91] +// - numers[i] = ∏_{j≠i} (100-j) +// +// Expected values (computed via ∏_{j≠i}): +// +// numers[0] = 628156509555294720 +// numers[1] = 634501524803328000 +// numers[2] = 640976030158464000 +// numers[3] = 647584030469376000 +// numers[4] = 654329697453432000 +// numers[5] = 661217378479257600 +// numers[6] = 668251605909888000 +// numers[7] = 675437107048704000 +// numers[8] = 682778814734016000 +// numers[9] = 690281878632192000 +func TestComputeNumeratorsBinaryTreeTenElements(t *testing.T) { + const m = 10 + const c = int64(100) + + // Verify against naive reference for both curves. + t.Run("BLS12381", func(t *testing.T) { + input := buildCMinusJBLS(c, m) + got := blsNumeratorsBinaryTree(c, m) + want := naiveNumeratorsBLS(input, m) + + require.Len(t, got, m) + for i := range m { + assert.True(t, equalsBLS(got[i], want[i]), + "m=%d c=%d BLS: index %d mismatch", m, c, i) + } + }) + + t.Run("BN254", func(t *testing.T) { + input := buildCMinusJBN254(c, m) + got := bn254NumeratorsBinaryTree(c, m) + want := naiveNumeratorsBN254(input, m) + + require.Len(t, got, m) + for i := range m { + assert.True(t, equalsBN254(got[i], want[i]), + "m=%d c=%d BN254: index %d mismatch", m, c, i) + } + }) + + // Spot-check hand-computed expected values for BLS12-381. + gotBLS := blsNumeratorsBinaryTree(c, m) + expected := []int64{ + 628156509555294720, + 634501524803328000, + 640976030158464000, + 647584030469376000, + 654329697453432000, + 661217378479257600, + 668251605909888000, + 675437107048704000, + 682778814734016000, + 690281878632192000, + } + for i, exp := range expected { + var e bls12381fr.Element + e.SetInt64(exp) + assert.True(t, equalsBLS(gotBLS[i], &e), + "m=%d c=%d: numers[%d] should be %d", m, c, i, exp) + } +} diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_native.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_native.go index e145b81369..af745fcd8b 100644 --- a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_native.go +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_native.go @@ -13,6 +13,122 @@ import ( bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) +// leftChild returns the index of the left child of node i in the tree array. +func leftChild(i int) int { + return 2*i + 1 +} + +// rightChild returns the index of the right child of node i in the tree array. +func rightChild(i int) int { + return 2*i + 2 +} + +// computeNumeratorsBinaryTree computes the numerators for Lagrange interpolation +// using a binary tree approach. For each leaf i, it computes the product of all +// (c-j) for j != i. +// +// It first writes c-j for j in [0,m) into the leaves region of pooled.slab +// (slab[leafStart:treeSize]). pooled.tree and pooled.leaves are contiguous in +// the slab, so a single fullE pointer array of length treeSize covers the +// entire tree with no leaf/internal distinction needed at read time. +// +// The slab layout is: +// +// slab = [ tree (leafStart) | leaves (m) | exclude (leafStart) | numers (m) ] +// |<---------- treeSize ---------->|<---------- treeSize ----------->| +// +// slab[treeSize:] mirrors the tree shape: internal-node slots hold exclude +// values and leaf slots hold numerator outputs. A single excludeE pointer array +// of length treeSize covering slab[treeSize:] lets the top-down pass write to +// either region with a plain index — no branch on whether a child is a leaf. +// +// Algorithm: +// 0. Initialise leaves: slab[leafStart+j] = c - j, for j in [0,m). +// 1. Build fullE[0..treeSize-1]: pointers into slab[0:treeSize] (tree+leaves). +// 2. Build excludeE[0..treeSize-1]: pointers into slab[treeSize:] (exclude+numers). +// 3. Bottom-up: for each internal node, multiply its two children's values. +// 4. Top-down: propagate exclude products; excludeE[child] receives the result +// directly — if child < leafStart it lands in exclude, otherwise in numers. +func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](m int, c *mathlib.Zr, pooled *treeArrays[T]) []E { + leafStart := m - 1 + treeSize := 2*m - 1 + + // fullE: pointers into slab[0:treeSize] — the bottom-up tree (internal + leaves). + fullE := make([]E, treeSize) + // excludeE: pointers into slab[treeSize:2*treeSize] — mirrors the tree shape. + excludeE := make([]E, treeSize) + for i := range treeSize { + fullE[i] = E(&pooled.slab[i]) + excludeE[i] = E(&pooled.slab[treeSize+i]) + } + + m2 := m / 2 + cE := math2.NativeFromZr[T, E](c) + var jE T + + if m&1 == 1 { + E(&jE).SetInt64(int64(m2)) + fullE[leafStart].Sub(cE, E(&jE)) + } + for i := range m2 { + E(&jE).SetInt64(int64(i)) + fullE[leafStart+2*i+(m&1)].Sub(cE, E(&jE)) + E(&jE).SetInt64(int64(m - 1 - i)) + fullE[leafStart+2*i+1+(m&1)].Sub(cE, E(&jE)) + } + + // start of the inner nodes whose both children are leaves + leafPairsStart := leafStart - m2 + + // Phase 1: Bottom-up — compute subtree products for internal nodes. + var ccmT, cMinusMm1T T + ccmE := E(&ccmT) + E(&jE).SetInt64(int64(m - 1)) + E(&cMinusMm1T).Sub(cE, E(&jE)) + ccmE.Mul(cE, E(&cMinusMm1T)) + + for i := leafStart - 1; i >= leafPairsStart; i-- { + j := i - leafPairsStart + E(&jE).SetInt64(int64(j * (m - 1 - j))) + fullE[i].Add(ccmE, E(&jE)) + } + + for i := leafPairsStart - 1; i >= 0; i-- { + left := leftChild(i) + right := rightChild(i) + + // Both children exist: node = left × right. + fullE[i].Mul(fullE[left], fullE[right]) + } + + // Phase 2: Top-down — compute exclude products and write leaf numerators. + // Root's exclude is 1 (nothing excluded above it). + excludeE[0].SetOne() + + for i := range leafStart { + left := leftChild(i) + right := rightChild(i) + + // Both children exist. + // Left child's exclude = parent exclude × right subtree product. + // Right child's exclude = parent exclude × left subtree product. + // excludeE[child] lands in exclude if child < leafStart, numers otherwise. + excludeE[left].Mul(excludeE[i], fullE[right]) + excludeE[right].Mul(excludeE[i], fullE[left]) + } + + numersE := make([]E, m) + if m&1 == 1 { + numersE[m2] = E(&pooled.slab[treeSize+leafStart]) + } + for i := range m2 { + numersE[i] = E(&pooled.slab[treeSize+leafStart+2*i+(m&1)]) + numersE[m-1-i] = E(&pooled.slab[treeSize+leafStart+2*i+1+(m&1)]) + } + + return numersE +} + // getLagrangeMultipliersNative is the native fr.Element implementation of // getLagrangeMultipliers. Conversions between mathlib.Zr and fr.Element occur // only once at the boundary (once for input c, n+1 times for the output slice), @@ -23,33 +139,10 @@ import ( func getLagrangeMultipliersNative[T any, E math2.GnarkFr[T]](n uint64, c *mathlib.Zr, curve *mathlib.Curve, denomInvs []E) ([]*mathlib.Zr, error) { m := int(n) + 1 // #nosec G115 - // Convert c once. - cE := math2.NativeFromZr[T, E](c) - - // cMinusJ[j] = c - j for j = 0..n - cMinusJ := make([]T, m) - cMinusJE := make([]E, m) - for j := range m { - cMinusJE[j] = E(&cMinusJ[j]) - var jE T - E(&jE).SetInt64(int64(j)) - cMinusJE[j].Sub(cE, E(&jE)) - } - // Compute numerator for each Lagrange basis polynomial L_i(c). // Denominators come from the cache — no O(n²) recomputation. - numers := make([]T, m) - numersE := make([]E, m) - for i := range numers { - numersE[i] = E(&numers[i]) - numersE[i].SetOne() - for j := range m { - if j == i { - continue - } - numersE[i].Mul(numersE[i], cMinusJE[j]) - } - } + pooled := getTreeArrays[T](m) + numersE := computeNumeratorsBinaryTree[T, E](m, c, pooled) result := make([]*mathlib.Zr, m) for i := range m { @@ -57,6 +150,7 @@ func getLagrangeMultipliersNative[T any, E math2.GnarkFr[T]](n uint64, c *mathli E(&prod).Mul(numersE[i], denomInvs[i]) result[i] = math2.NativeToZr[T, E](E(&prod), curve) } + putTreeArrays(pooled) return result, nil } @@ -67,47 +161,26 @@ func getLagrangeMultipliersNative[T any, E math2.GnarkFr[T]](n uint64, c *mathli func getLagrangeMultipliersPartialNative[T any, E math2.GnarkFr[T]](n uint64, c *mathlib.Zr, curve *mathlib.Curve, denomInvs []E) ([]*mathlib.Zr, error) { total := 2*int(n) + 1 // #nosec G115 // all evaluation points: 0..2n - cE := math2.NativeFromZr[T, E](c) - - // cMinusJ[j] = c - j for j = 0..2n - cMinusJ := make([]T, total) - cMinusJE := make([]E, total) - for j := range total { - cMinusJE[j] = E(&cMinusJ[j]) - var jE T - E(&jE).SetInt64(int64(j)) - cMinusJE[j].Sub(cE, E(&jE)) - } - + // Compute numerators for all points, then extract relevant ones. // Relevant indices in the full point set: {0, n+1, n+2, ..., 2n} - relevant := make([]int, int(n)+1) // #nosec G115 - relevant[0] = 0 - for k := 1; k <= int(n); k++ { // #nosec G115 - relevant[k] = int(n) + k // #nosec G115 - } + // relevant[0]=0, relevant[k]=n+k for k>=1 — computed inline, no allocation needed. + pooled := getTreeArrays[T](total) + allNumersE := computeNumeratorsBinaryTree[T, E](total, c, pooled) - numers := make([]T, len(relevant)) - numersE := make([]E, len(relevant)) - for k := range relevant { - numersE[k] = E(&numers[k]) - } + result := make([]*mathlib.Zr, int(n)+1) // #nosec G115 - for k, i := range relevant { - numersE[k].SetOne() - for j := range total { - if j == i { - continue - } - numersE[k].Mul(numersE[k], cMinusJE[j]) - } - } + // k=0: relevant index is 0 + var prod0 T + E(&prod0).Mul(allNumersE[0], denomInvs[0]) + result[0] = math2.NativeToZr[T, E](E(&prod0), curve) - result := make([]*mathlib.Zr, len(relevant)) - for k := range relevant { + // k=1..n: relevant index is n+k + for k := 1; k <= int(n); k++ { // #nosec G115 var prod T - E(&prod).Mul(numersE[k], denomInvs[k]) + E(&prod).Mul(allNumersE[int(n)+k], denomInvs[k]) // #nosec G115 result[k] = math2.NativeToZr[T, E](E(&prod), curve) } + putTreeArrays(pooled) return result, nil } diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_numerators_test.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_numerators_test.go new file mode 100644 index 0000000000..9f4b530c57 --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_numerators_test.go @@ -0,0 +1,55 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package csp + +import ( + "fmt" + "testing" + + math "github.com/IBM/mathlib" + "github.com/stretchr/testify/require" +) + +// TestLagrangeNumeratorsVariousSizes verifies that the binary tree method +// produces correct numerators for various numbers of leaves, including +// edge cases like odd numbers and powers of 2. +func TestLagrangeNumeratorsVariousSizes(t *testing.T) { + curve := math.Curves[math.BN254] + rand, err := curve.Rand() + require.NoError(t, err) + + // Test various sizes including edge cases + testSizes := []int{23, 30, 31, 32, 33} + + for _, m := range testSizes { + t.Run(fmt.Sprintf("m=%d", m), func(t *testing.T) { + // Generate a random challenge c + cZr := curve.NewRandomZr(rand) + + // Compute numerators using binary tree method + binaryTreeResult, ok, err := nativeLagrangeMultipliers(uint64(m-1), cZr, curve) // #nosec G115 + require.NoError(t, err) + require.True(t, ok) + require.Len(t, binaryTreeResult, m) + + // Compute numerators using fallback method + fallbackResult, err := getLagrangeMultipliers(uint64(m-1), cZr, curve) // #nosec G115 + require.NoError(t, err) + require.Len(t, fallbackResult, m) + + // Verify they match + for i := range m { + require.True(t, binaryTreeResult[i].Equals(fallbackResult[i]), + "Mismatch at index %d for m=%d", i, m) + } + + t.Logf("✓ Verified m=%d: binary tree matches fallback", m) + }) + } +} + +// Made with Bob diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool.go new file mode 100644 index 0000000000..c91948bdc2 --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool.go @@ -0,0 +1,156 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package csp + +import ( + "sync" + + bls12381fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" +) + +// treeArrayPool manages memory pools for tree array slabs used in +// computeNumeratorsBinaryTree. Each slab holds all three working regions +// (tree, numers, exclude) in a single contiguous allocation, keyed by total +// slab size. This means one pool lookup, one GC-tracked object, and better +// cache locality compared to three separate allocations. +type treeArrayPool[T any] struct { + pools map[int]*sync.Pool // key: total slab size (m + 2*leafStart) + mu sync.RWMutex +} + +// newTreeArrayPool creates a new pool for slabs of type T. +func newTreeArrayPool[T any]() *treeArrayPool[T] { + return &treeArrayPool[T]{ + pools: make(map[int]*sync.Pool), + } +} + +// getPool returns the sync.Pool for the given total size, creating it if necessary. +func (p *treeArrayPool[T]) getPool(size int) *sync.Pool { + p.mu.RLock() + pool, exists := p.pools[size] + p.mu.RUnlock() + + if exists { + return pool + } + + // Double-checked locking: create pool if still absent after acquiring write lock. + p.mu.Lock() + defer p.mu.Unlock() + + if pool, exists := p.pools[size]; exists { + return pool + } + + pool = &sync.Pool{ + New: func() any { + s := make([]T, size) + + return &s + }, + } + p.pools[size] = pool + + return pool +} + +// Global pools for BLS12-381 and BN254 curves +var ( + blsTreePool = newTreeArrayPool[bls12381fr.Element]() + bn254TreePool = newTreeArrayPool[bn254fr.Element]() +) + +// treeArrays is a single contiguous slab used by the binary-tree numerator algorithm. +// +// Layout: +// +// slab = [ tree+leaves (treeSize) | exclude+numers (treeSize) ] +// slab[0:treeSize] slab[treeSize:2*treeSize] +// +// Within the first half: slab[0:leafStart] are internal-node products (tree), +// slab[leafStart:treeSize] are the c-j input values (leaves). +// Within the second half: slab[treeSize:treeSize+leafStart] are exclude products, +// slab[treeSize+leafStart:] are the output numerators. +// +// Callers index slab directly using leafStart and treeSize, which they already +// hold — no sub-slice fields needed. +type treeArrays[T any] struct { + slab []T // backing allocation returned to pool on put + pool *treeArrayPool[T] +} + +// getTreeArrays retrieves a pooled slab of length 2*treeSize for type T. +// m is the number of leaves; leafStart = m-1 is derived internally. +func getTreeArrays[T any](m int) *treeArrays[T] { + treeSize := 2*m - 1 + total := 2 * treeSize + pool := getTypedPool[T]() + raw := pool.getPool(total).Get() + var slab []T + if sp, ok := raw.(*[]T); ok && len(*sp) == total { + slab = *sp + } else { + slab = make([]T, total) + } + + return &treeArrays[T]{ + slab: slab, + pool: pool, + } +} + +// putTreeArrays returns the slab inside ta back to its pool. +func putTreeArrays[T any](ta *treeArrays[T]) { + if ta == nil { + return + } + pool := ta.pool.getPool(len(ta.slab)) + pool.Put(&ta.slab) +} + +// typedPools is a registry mapping element-type name to an untyped pool wrapper. +// We use a two-level approach: a map from type key → *treeArrayPool[T] stored +// as any, unwrapped via a typed accessor registered at init time. +var typedPoolRegistry = map[string]any{} // populated by registerTypedPool + +// getTypedPool returns the *treeArrayPool[T] for type T. +// It panics if no pool has been registered for T. +func getTypedPool[T any]() *treeArrayPool[T] { + var zero T + key := typedPoolKey(zero) + p, ok := typedPoolRegistry[key] + if !ok { + panic("csp: no treeArrayPool registered for type " + key) + } + + return p.(*treeArrayPool[T]) +} + +// typedPoolKey returns a string key for a value of any type, used to look up +// the pool in typedPoolRegistry. +func typedPoolKey(v any) string { + return typeOf(v) +} + +// typeOf returns a stable string name for the dynamic type of v. +func typeOf(v any) string { + switch v.(type) { + case bls12381fr.Element: + return "bls12381fr.Element" + case bn254fr.Element: + return "bn254fr.Element" + default: + panic("csp: unsupported element type") + } +} + +func init() { + typedPoolRegistry["bls12381fr.Element"] = blsTreePool + typedPoolRegistry["bn254fr.Element"] = bn254TreePool +} diff --git a/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool_test.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool_test.go new file mode 100644 index 0000000000..3739b0ade3 --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_pool_test.go @@ -0,0 +1,160 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package csp + +import ( + "testing" + + bls12381fr "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/stretchr/testify/require" +) + +func TestTreeArrayPoolBLS(t *testing.T) { + // Test basic get/put operations + m := 65 + treeSize := 2*m - 1 + + arrays := getTreeArrays[bls12381fr.Element](m) + require.NotNil(t, arrays) + require.Len(t, arrays.slab, 2*treeSize) + + // Return to pool + putTreeArrays(arrays) + + // Get again - should reuse from pool + arrays2 := getTreeArrays[bls12381fr.Element](m) + require.NotNil(t, arrays2) + require.Len(t, arrays2.slab, 2*treeSize) + + putTreeArrays(arrays2) +} + +func TestTreeArrayPoolBN254(t *testing.T) { + // Test basic get/put operations + m := 129 + treeSize := 2*m - 1 + + arrays := getTreeArrays[bn254fr.Element](m) + require.NotNil(t, arrays) + require.Len(t, arrays.slab, 2*treeSize) + + // Return to pool + putTreeArrays(arrays) + + // Get again - should reuse from pool + arrays2 := getTreeArrays[bn254fr.Element](m) + require.NotNil(t, arrays2) + require.Len(t, arrays2.slab, 2*treeSize) + + putTreeArrays(arrays2) +} + +func TestTreeArrayPoolConcurrent(t *testing.T) { + // Test concurrent access to pool + const goroutines = 10 + const iterations = 100 + const m = 65 + const leafStart = m - 1 + const treeSize = 2*m - 1 + + done := make(chan bool, goroutines) + + for range goroutines { + go func() { + for range iterations { + arrays := getTreeArrays[bls12381fr.Element](m) + // Simulate some work using direct slab offsets. + // slab[0] → tree[0] + // slab[treeSize] → exclude[0] + // slab[treeSize+leafStart] → numers[0] + arrays.slab[0].SetOne() + arrays.slab[treeSize].SetZero() + arrays.slab[treeSize+leafStart].SetOne() + putTreeArrays(arrays) + } + done <- true + }() + } + + // Wait for all goroutines + for range goroutines { + <-done + } +} + +func TestTreeArrayPoolDifferentSizes(t *testing.T) { + // Test that different sizes use different pools + ms := []int{33, 65, 129, 257} + + for _, m := range ms { + treeSize := 2*m - 1 + arrays := getTreeArrays[bls12381fr.Element](m) + require.Len(t, arrays.slab, 2*treeSize) + putTreeArrays(arrays) + } +} + +// BenchmarkTreeArrayPoolBLS benchmarks the pool performance for BLS12-381 +func BenchmarkTreeArrayPoolBLS(b *testing.B) { + const m = 65 + const leafStart = m - 1 + + b.Run("with_pool", func(b *testing.B) { + b.ReportAllocs() + for range b.N { + arrays := getTreeArrays[bls12381fr.Element](m) + // Simulate some work: touch tree[0] at slab[0]. + arrays.slab[0].SetOne() + putTreeArrays(arrays) + } + }) + + b.Run("without_pool", func(b *testing.B) { + b.ReportAllocs() + for range b.N { + tree := make([]bls12381fr.Element, leafStart) + numers := make([]bls12381fr.Element, m) + exclude := make([]bls12381fr.Element, leafStart) + // Simulate some work + tree[0].SetOne() + _ = numers + _ = exclude + } + }) +} + +// BenchmarkTreeArrayPoolBN254 benchmarks the pool performance for BN254 +func BenchmarkTreeArrayPoolBN254(b *testing.B) { + const m = 129 + const leafStart = m - 1 + + b.Run("with_pool", func(b *testing.B) { + b.ReportAllocs() + for range b.N { + arrays := getTreeArrays[bn254fr.Element](m) + // Simulate some work: touch tree[0] at slab[0]. + arrays.slab[0].SetOne() + putTreeArrays(arrays) + } + }) + + b.Run("without_pool", func(b *testing.B) { + b.ReportAllocs() + for range b.N { + tree := make([]bn254fr.Element, leafStart) + numers := make([]bn254fr.Element, m) + exclude := make([]bn254fr.Element, leafStart) + // Simulate some work + tree[0].SetOne() + _ = numers + _ = exclude + } + }) +} + +// Made with Bob