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_binary_tree_test.go b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go new file mode 100644 index 0000000000..19bf58321b --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go @@ -0,0 +1,273 @@ +/* +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/assert" + "github.com/stretchr/testify/require" +) + +// 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 the original O(n²) method 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], matching computeNumeratorsOriginal. +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 := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, m) + want := computeNumeratorsOriginal[bls12381fr.Element, *bls12381fr.Element](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 is the BN254 counterpart of +// TestComputeNumeratorsBinaryTreeVsOriginalBLS. +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 := computeNumeratorsBinaryTree[bn254fr.Element, *bn254fr.Element](input, m) + want := computeNumeratorsOriginal[bn254fr.Element, *bn254fr.Element](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 + input := buildCMinusJBLS(10, 2) + + got := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, 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) { + input := buildCMinusJBLS(10, 3) + + got := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, 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) { + input := buildCMinusJBLS(10, 4) + + got := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, 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} { + input := buildCMinusJBLS(1000, m) + got := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, 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 + + input := buildCMinusJBLS(c, m) + got := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, 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) + + input := buildCMinusJBLS(c, m) + + // snapshot input values before the call + snapshot := make([]bls12381fr.Element, m) + for i, e := range input { + snapshot[i] = *e + } + + _ = computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, m) + + // verify input is unchanged + for i, e := range input { + assert.True(t, equalsBLS(e, &snapshot[i]), + "input element %d was mutated by computeNumeratorsBinaryTree", 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 := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, m) + want := computeNumeratorsOriginal[bls12381fr.Element, *bls12381fr.Element](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 := computeNumeratorsBinaryTree[bn254fr.Element, *bn254fr.Element](input, m) + want := computeNumeratorsOriginal[bn254fr.Element, *bn254fr.Element](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 := computeNumeratorsBinaryTree[bls12381fr.Element, *bls12381fr.Element](input, m) + want := computeNumeratorsOriginal[bls12381fr.Element, *bls12381fr.Element](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) + } +} 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..e387d64bab 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,176 @@ import ( bn254fr "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) +// UseBinaryTreeNumerator controls which numerator computation method to use. +// true: use binary tree optimization (new method) +// false: use product-then-divide method (original method) +var UseBinaryTreeNumerator = true + +// binaryTreeSize returns the size of a binary tree array for n leaves. +// A binary tree with n leaves has 2n-1 total nodes. +func binaryTreeSize(n int) int { + return 2*n - 1 +} + +// 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. +// +// Algorithm: +// 1. Build a binary tree where leaf i holds (c-i) +// 2. Bottom-up: compute product of all leaves in each subtree +// 3. Top-down: compute product of all leaves except those in subtree +// 4. At leaves, we have the desired numerators +// +// Optimization: Leaves are not stored in the tree array; they are accessed +// directly from cMinusJE when needed. Only internal nodes are allocated. +// Exclude values for leaves are written directly to the output numers array. +func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) []E { + treeSize := binaryTreeSize(m) + leafStart := treeSize - m + + // Helper to check if index is a leaf + isLeaf := func(i int) bool { + return i >= leafStart + } + + // Helper to get leaf value from cMinusJE + getLeafValue := func(i int) E { + return cMinusJE[i-leafStart] + } + + // Allocate tree arrays (only for internal nodes, not leaves) + // Internal nodes are at indices [0, leafStart) + tree := make([]T, leafStart) + treeE := make([]E, leafStart) + for i := range tree { + treeE[i] = E(&tree[i]) + } + + // Allocate output numerators array (for leaves) + numers := make([]T, m) + numersE := make([]E, m) + for i := range numers { + numersE[i] = E(&numers[i]) + } + + // Exclude array only needs space for internal nodes + // Leaf exclude values are written directly to numers + exclude := make([]T, leafStart) + excludeE := make([]E, leafStart) + for i := range exclude { + excludeE[i] = E(&exclude[i]) + } + + // Phase 1: Bottom-up - compute subtree products + // Process from last internal node down to root + for i := leafStart - 1; i >= 0; i-- { + left := leftChild(i) + right := rightChild(i) + + if right < treeSize { + // Both children exist + var leftVal, rightVal E + if isLeaf(left) { + leftVal = getLeafValue(left) + } else { + leftVal = treeE[left] + } + if isLeaf(right) { + rightVal = getLeafValue(right) + } else { + rightVal = treeE[right] + } + treeE[i].Mul(leftVal, rightVal) + } else if left < treeSize { + // Only left child exists (can happen in non-perfect binary tree) + if isLeaf(left) { + tree[i] = *getLeafValue(left) + } else { + tree[i] = tree[left] + } + } else { + // This shouldn't happen in a properly constructed tree + treeE[i].SetOne() + } + } + + // Phase 2: Top-down - compute exclude products + // Root's exclude product is 1 (no leaves to exclude) + excludeE[0].SetOne() + + // Process from root down to leaves + for i := range leafStart { + left := leftChild(i) + right := rightChild(i) + + if right < treeSize { + // Both children exist + var leftVal, rightVal E + if isLeaf(left) { + leftVal = getLeafValue(left) + } else { + leftVal = treeE[left] + } + if isLeaf(right) { + rightVal = getLeafValue(right) + } else { + rightVal = treeE[right] + } + // Left child excludes: parent's exclude × right subtree + if isLeaf(left) { + numersE[left-leafStart].Mul(excludeE[i], rightVal) + } else { + excludeE[left].Mul(excludeE[i], rightVal) + } + // Right child excludes: parent's exclude × left subtree + if isLeaf(right) { + numersE[right-leafStart].Mul(excludeE[i], leftVal) + } else { + excludeE[right].Mul(excludeE[i], leftVal) + } + } else if left < treeSize { + // Only left child exists + if isLeaf(left) { + numers[left-leafStart] = exclude[i] + } else { + exclude[left] = exclude[i] + } + } + } + + return numersE +} + +// computeNumeratorsOriginal computes the numerators using the original method: +// compute full product, then divide out each element. +func computeNumeratorsOriginal[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) []E { + 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]) + } + } + + 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), @@ -38,17 +208,11 @@ func getLagrangeMultipliersNative[T any, E math2.GnarkFr[T]](n uint64, c *mathli // 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]) - } + var numersE []E + if UseBinaryTreeNumerator { + numersE = computeNumeratorsBinaryTree[T, E](cMinusJE, m) + } else { + numersE = computeNumeratorsOriginal[T, E](cMinusJE, m) } result := make([]*mathlib.Zr, m) @@ -86,20 +250,20 @@ func getLagrangeMultipliersPartialNative[T any, E math2.GnarkFr[T]](n uint64, c relevant[k] = int(n) + k // #nosec G115 } - numers := make([]T, len(relevant)) - numersE := make([]E, len(relevant)) - for k := range relevant { - numersE[k] = E(&numers[k]) + // Compute numerators for all points, then extract relevant ones + var allNumersE []E + if UseBinaryTreeNumerator { + allNumersE = computeNumeratorsBinaryTree[T, E](cMinusJE, total) + } else { + allNumersE = computeNumeratorsOriginal[T, E](cMinusJE, total) } + // Extract numerators for relevant indices + numers := make([]T, len(relevant)) + numersE := make([]E, len(relevant)) for k, i := range relevant { - numersE[k].SetOne() - for j := range total { - if j == i { - continue - } - numersE[k].Mul(numersE[k], cMinusJE[j]) - } + numersE[k] = E(&numers[k]) + numers[k] = *allNumersE[i] } result := make([]*mathlib.Zr, len(relevant))