From 72b5b61953dc4d68d413e0666090e4bea7d1b0a4 Mon Sep 17 00:00:00 2001 From: Hayim Shaul Date: Wed, 24 Jun 2026 06:10:03 +0000 Subject: [PATCH 01/11] optimize lagrange numerator Signed-off-by: Hayim Shaul --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 177 +++++++++++++++--- 1 file changed, 155 insertions(+), 22 deletions(-) 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..847b4f1d6a 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,145 @@ 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 +} + +// parent returns the index of the parent of node i in the tree array. +func parent(i int) int { + return (i - 1) / 2 +} + +// sibling returns the index of the sibling of node i in the tree array. +func sibling(i int) int { + if i == 0 { + return -1 // root has no sibling + } + p := parent(i) + left := leftChild(p) + if i == left { + return rightChild(p) + } + return left +} + +// 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 +func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) []E { + treeSize := binaryTreeSize(m) + + // Allocate tree arrays + tree := make([]T, treeSize) + treeE := make([]E, treeSize) + for i := range tree { + treeE[i] = E(&tree[i]) + } + + exclude := make([]T, treeSize) + excludeE := make([]E, treeSize) + for i := range exclude { + excludeE[i] = E(&exclude[i]) + } + + // Initialize leaves with (c-j) values + // Leaves are stored at indices [treeSize-m, treeSize) + leafStart := treeSize - m + for i := 0; i < m; i++ { + tree[leafStart+i] = *cMinusJE[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 + treeE[i].Mul(treeE[left], treeE[right]) + } else if left < treeSize { + // Only left child exists (can happen in non-perfect binary tree) + 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 := 0; i < leafStart; i++ { + left := leftChild(i) + right := rightChild(i) + + if right < treeSize { + // Both children exist + // Left child excludes: parent's exclude × right subtree + excludeE[left].Mul(excludeE[i], treeE[right]) + // Right child excludes: parent's exclude × left subtree + excludeE[right].Mul(excludeE[i], treeE[left]) + } else if left < treeSize { + // Only left child exists + exclude[left] = exclude[i] + } + } + + // Extract numerators from leaf exclude values + numers := make([]T, m) + numersE := make([]E, m) + for i := 0; i < m; i++ { + numersE[i] = E(&numers[i]) + numers[i] = exclude[leafStart+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 +177,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 +219,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)) From f636de60dee63cb453e3a8e8821ebbfa69646c7d Mon Sep 17 00:00:00 2001 From: Hayim Shaul Date: Wed, 24 Jun 2026 06:25:40 +0000 Subject: [PATCH 02/11] reduce alloc size Signed-off-by: Hayim Shaul --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) 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 847b4f1d6a..c11b6cd67d 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 @@ -61,29 +61,39 @@ func sibling(i int) int { // 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. 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 + } - // Allocate tree arrays - tree := make([]T, treeSize) - treeE := make([]E, treeSize) + // 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]) } + // Exclude array needs space for both internal nodes and leaves + // because we compute exclude values for leaves in phase 2 exclude := make([]T, treeSize) excludeE := make([]E, treeSize) for i := range exclude { excludeE[i] = E(&exclude[i]) } - // Initialize leaves with (c-j) values - // Leaves are stored at indices [treeSize-m, treeSize) - leafStart := treeSize - m - for i := 0; i < m; i++ { - tree[leafStart+i] = *cMinusJE[i] - } - // Phase 1: Bottom-up - compute subtree products // Process from last internal node down to root for i := leafStart - 1; i >= 0; i-- { @@ -92,10 +102,25 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) if right < treeSize { // Both children exist - treeE[i].Mul(treeE[left], treeE[right]) + 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) - tree[i] = tree[left] + 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() @@ -113,10 +138,21 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) 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 - excludeE[left].Mul(excludeE[i], treeE[right]) + excludeE[left].Mul(excludeE[i], rightVal) // Right child excludes: parent's exclude × left subtree - excludeE[right].Mul(excludeE[i], treeE[left]) + excludeE[right].Mul(excludeE[i], leftVal) } else if left < treeSize { // Only left child exists exclude[left] = exclude[i] From 853084dfcfad8d0b00cf236554347668f0650f0c Mon Sep 17 00:00:00 2001 From: Hayim Shaul Date: Wed, 24 Jun 2026 07:10:55 +0000 Subject: [PATCH 03/11] dont copy leaves at 2nd step Signed-off-by: Hayim Shaul --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 86 +++++++++++++++---- 1 file changed, 67 insertions(+), 19 deletions(-) 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 c11b6cd67d..a11740aa84 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 @@ -64,6 +64,7 @@ func sibling(i int) int { // // 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 @@ -86,10 +87,17 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) treeE[i] = E(&tree[i]) } - // Exclude array needs space for both internal nodes and leaves - // because we compute exclude values for leaves in phase 2 - exclude := make([]T, treeSize) - excludeE := make([]E, treeSize) + // 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]) } @@ -128,11 +136,47 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) } // Phase 2: Top-down - compute exclude products - // Root's exclude product is 1 (no leaves to exclude) - excludeE[0].SetOne() + // Start with root's children (skip root since its exclude product is 1) + left := leftChild(0) + right := rightChild(0) + + // Initialize root's children + 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 right subtree (parent's exclude=1, so just right subtree) + if isLeaf(left) { + numers[left-leafStart] = *rightVal + } else { + exclude[left] = *rightVal + } + // Right child excludes left subtree (parent's exclude=1, so just left subtree) + if isLeaf(right) { + numers[right-leafStart] = *leftVal + } else { + exclude[right] = *leftVal + } + } else if left < treeSize { + // Only left child exists (exclude product is 1) + if isLeaf(left) { + numersE[left-leafStart].SetOne() + } else { + excludeE[left].SetOne() + } + } - // Process from root down to leaves - for i := 0; i < leafStart; i++ { + // Process remaining nodes from level 1 down to leaves + for i := 1; i < leafStart; i++ { left := leftChild(i) right := rightChild(i) @@ -150,23 +194,27 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) rightVal = treeE[right] } // Left child excludes: parent's exclude × right subtree - excludeE[left].Mul(excludeE[i], rightVal) + 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 - excludeE[right].Mul(excludeE[i], leftVal) + 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 - exclude[left] = exclude[i] + if isLeaf(left) { + numers[left-leafStart] = exclude[i] + } else { + exclude[left] = exclude[i] + } } } - // Extract numerators from leaf exclude values - numers := make([]T, m) - numersE := make([]E, m) - for i := 0; i < m; i++ { - numersE[i] = E(&numers[i]) - numers[i] = exclude[leafStart+i] - } - return numersE } From 6efaa35bc12a30d8a9837c644319d76a1d31dcf8 Mon Sep 17 00:00:00 2001 From: Hayim Shaul Date: Wed, 24 Jun 2026 07:34:14 +0000 Subject: [PATCH 04/11] no special treetment for root in 2nd step Signed-off-by: Hayim Shaul --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) 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 a11740aa84..4c1d8caf8e 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 @@ -136,47 +136,11 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) } // Phase 2: Top-down - compute exclude products - // Start with root's children (skip root since its exclude product is 1) - left := leftChild(0) - right := rightChild(0) + // Root's exclude product is 1 (no leaves to exclude) + excludeE[0].SetOne() - // Initialize root's children - 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 right subtree (parent's exclude=1, so just right subtree) - if isLeaf(left) { - numers[left-leafStart] = *rightVal - } else { - exclude[left] = *rightVal - } - // Right child excludes left subtree (parent's exclude=1, so just left subtree) - if isLeaf(right) { - numers[right-leafStart] = *leftVal - } else { - exclude[right] = *leftVal - } - } else if left < treeSize { - // Only left child exists (exclude product is 1) - if isLeaf(left) { - numersE[left-leafStart].SetOne() - } else { - excludeE[left].SetOne() - } - } - - // Process remaining nodes from level 1 down to leaves - for i := 1; i < leafStart; i++ { + // Process from root down to leaves + for i := 0; i < leafStart; i++ { left := leftChild(i) right := rightChild(i) From 60c9a59a42ad9610e84c86fb1e0aecdf24cdf4a8 Mon Sep 17 00:00:00 2001 From: Eyal Kushnir <49232950+kushnireyal@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:30:03 +0300 Subject: [PATCH 05/11] optimize CSP verify: fold padding generators and merge final MSMs (#1833) Signed-off-by: Eyal Kushnir Co-authored-by: Eyal Kushnir Signed-off-by: Hayim Shaul --- .../zkatdlog/nogh/v1/crypto/rp/csp/csp.go | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) 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") } From 710320a2fc2d7ba4d0c67c0f71256532dac0acb3 Mon Sep 17 00:00:00 2001 From: Hayim Shaul Date: Wed, 1 Jul 2026 14:46:15 +0000 Subject: [PATCH 06/11] fmt Signed-off-by: Hayim Shaul --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 4c1d8caf8e..07090c9be1 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 @@ -68,17 +68,17 @@ func sibling(i int) int { 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) @@ -86,14 +86,14 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) 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) @@ -101,13 +101,13 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) 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 @@ -134,16 +134,16 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) 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 := 0; i < leafStart; i++ { left := leftChild(i) right := rightChild(i) - + if right < treeSize { // Both children exist var leftVal, rightVal E @@ -178,7 +178,7 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) } } } - + return numersE } From 7305100d29127e61568419286589294e9a3c5cc1 Mon Sep 17 00:00:00 2001 From: "Hayim.Shaul@ibm.com" Date: Wed, 1 Jul 2026 14:55:38 +0000 Subject: [PATCH 07/11] lint Signed-off-by: Hayim.Shaul@ibm.com --- .../nogh/v1/crypto/rp/csp/lagrange_native.go | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) 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 07090c9be1..0736171a31 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 @@ -34,24 +34,6 @@ func rightChild(i int) int { return 2*i + 2 } -// parent returns the index of the parent of node i in the tree array. -func parent(i int) int { - return (i - 1) / 2 -} - -// sibling returns the index of the sibling of node i in the tree array. -func sibling(i int) int { - if i == 0 { - return -1 // root has no sibling - } - p := parent(i) - left := leftChild(p) - if i == left { - return rightChild(p) - } - return left -} - // 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. @@ -140,7 +122,7 @@ func computeNumeratorsBinaryTree[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) excludeE[0].SetOne() // Process from root down to leaves - for i := 0; i < leafStart; i++ { + for i := range leafStart { left := leftChild(i) right := rightChild(i) @@ -197,6 +179,7 @@ func computeNumeratorsOriginal[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) [ numersE[i].Mul(numersE[i], cMinusJE[j]) } } + return numersE } From cd5c02b9e7fb8120047fa0f04047321b9dd22365 Mon Sep 17 00:00:00 2001 From: "Hayim.Shaul@ibm.com" Date: Wed, 1 Jul 2026 14:59:57 +0000 Subject: [PATCH 08/11] fmt Signed-off-by: Hayim.Shaul@ibm.com --- token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_native.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0736171a31..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 @@ -179,7 +179,7 @@ func computeNumeratorsOriginal[T any, E math2.GnarkFr[T]](cMinusJE []E, m int) [ numersE[i].Mul(numersE[i], cMinusJE[j]) } } - + return numersE } From 1e47f45767f4bc12890d4a5f2c5cddbd75bc1cb1 Mon Sep 17 00:00:00 2001 From: "Hayim.Shaul@ibm.com" Date: Thu, 2 Jul 2026 06:03:27 +0000 Subject: [PATCH 09/11] added tests Signed-off-by: Hayim.Shaul@ibm.com --- .../rp/csp/lagrange_binary_tree_test.go | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go 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..36e5e80c46 --- /dev/null +++ b/token/core/zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go @@ -0,0 +1,271 @@ +/* +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) + } +} From d26f6c1a0ea1e4cab08785690c32a0fa9105e24e Mon Sep 17 00:00:00 2001 From: "Hayim.Shaul@ibm.com" Date: Thu, 2 Jul 2026 06:12:39 +0000 Subject: [PATCH 10/11] lint Signed-off-by: Hayim.Shaul@ibm.com --- .../zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go | 2 ++ 1 file changed, 2 insertions(+) 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 index 36e5e80c46..180aba6fe5 100644 --- 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 @@ -24,6 +24,7 @@ func buildCMinusJBLS(c int64, m int) []*bls12381fr.Element { e.SetInt64(c - int64(j)) // #nosec G115 elems[j] = e } + return elems } @@ -34,6 +35,7 @@ func buildCMinusJBN254(c int64, m int) []*bn254fr.Element { e.SetInt64(c - int64(j)) // #nosec G115 elems[j] = e } + return elems } From f1f90f0a7e8516c15baab9831754fb87ba60c01a Mon Sep 17 00:00:00 2001 From: "Hayim.Shaul@ibm.com" Date: Thu, 2 Jul 2026 06:18:59 +0000 Subject: [PATCH 11/11] fmt Signed-off-by: Hayim.Shaul@ibm.com --- .../zkatdlog/nogh/v1/crypto/rp/csp/lagrange_binary_tree_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 180aba6fe5..19bf58321b 100644 --- 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 @@ -35,7 +35,7 @@ func buildCMinusJBN254(c int64, m int) []*bn254fr.Element { e.SetInt64(c - int64(j)) // #nosec G115 elems[j] = e } - + return elems }