Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions addpairsofproducts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package math

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func runAddPairsOfProductsTest(t *testing.T, c *Curve) {
Comment thread
HayimShaul marked this conversation as resolved.
rng, err := c.Rand()
require.NoError(t, err)

// Test with different sizes
testSizes := []int{1, 2, 5, 10}

for _, n := range testSizes {
t.Run("size_"+string(rune(n+'0')), func(t *testing.T) {
// Generate random scalars and generators
left := make([]*Zr, n)
right := make([]*Zr, n)
leftgen := make([]*G1, n)
rightgen := make([]*G1, n)

for i := 0; i < n; i++ {
left[i] = c.NewRandomZr(rng)
right[i] = c.NewRandomZr(rng)
leftgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
rightgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
}

// Compute using AddPairsOfProducts
result := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Compute manually to verify correctness
// result should equal: sum of (left[i] * leftgen[i] + right[i] * rightgen[i])
// Use Mul2 for each pair (which computes left[i]*leftgen[i] + right[i]*rightgen[i])
expected := leftgen[0].Mul2(left[0], rightgen[0], right[0])

// Add remaining pairs
for i := 1; i < n; i++ {
pairResult := leftgen[i].Mul2(left[i], rightgen[i], right[i])
expected.Add(pairResult)
}

assert.True(t, result.Equals(expected), "AddPairsOfProducts result does not match expected value for curve %s with size %d", CurveIDToString(c.curveID), n)
})
}
}

// TestAddPairsOfProductsEdgeCases tests edge cases for AddPairsOfProducts
func TestAddPairsOfProductsEdgeCases(t *testing.T) {
for _, curve := range Curves {
t.Run(CurveIDToString(curve.curveID), func(t *testing.T) {
runAddPairsOfProductsEdgeCasesTest(t, curve)
})
}
}

func runAddPairsOfProductsEdgeCasesTest(t *testing.T, c *Curve) {
rng, err := c.Rand()
require.NoError(t, err)

// Test with zero scalars
t.Run("zero_scalars", func(t *testing.T) {
n := 3
left := make([]*Zr, n)
right := make([]*Zr, n)
leftgen := make([]*G1, n)
rightgen := make([]*G1, n)

for i := 0; i < n; i++ {
left[i] = c.NewZrFromInt(0)
right[i] = c.NewZrFromInt(0)
leftgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
rightgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
}

result := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Result should be the identity element (infinity)
assert.True(t, result.IsInfinity(), "Result should be infinity when all scalars are zero for curve %s", CurveIDToString(c.curveID))
})

// Test with identity generators
t.Run("identity_generators", func(t *testing.T) {
n := 2
left := make([]*Zr, n)
right := make([]*Zr, n)
leftgen := make([]*G1, n)
rightgen := make([]*G1, n)

for i := 0; i < n; i++ {
left[i] = c.NewRandomZr(rng)
right[i] = c.NewRandomZr(rng)
// Use the base generator
leftgen[i] = c.GenG1.Copy()
rightgen[i] = c.GenG1.Copy()
}

result := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Manually compute expected result using Mul2
expected := c.GenG1.Mul2(left[0], c.GenG1, right[0])
for i := 1; i < n; i++ {
pairResult := c.GenG1.Mul2(left[i], c.GenG1, right[i])
expected.Add(pairResult)
}

assert.True(t, result.Equals(expected), "Result mismatch with identity generators for curve %s", CurveIDToString(c.curveID))
})

// Test with one element
t.Run("single_element", func(t *testing.T) {
left := []*Zr{c.NewRandomZr(rng)}
right := []*Zr{c.NewRandomZr(rng)}
leftgen := []*G1{c.GenG1.Mul(c.NewRandomZr(rng))}
rightgen := []*G1{c.GenG1.Mul(c.NewRandomZr(rng))}

result := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Expected: left[0] * leftgen[0] + right[0] * rightgen[0]
expected := leftgen[0].Mul2(left[0], rightgen[0], right[0])

assert.True(t, result.Equals(expected), "Result mismatch for single element for curve %s", CurveIDToString(c.curveID))
})

// Test commutativity: swapping pairs should give same result
t.Run("commutativity", func(t *testing.T) {
n := 3
left := make([]*Zr, n)
right := make([]*Zr, n)
leftgen := make([]*G1, n)
rightgen := make([]*G1, n)

for i := 0; i < n; i++ {
left[i] = c.NewRandomZr(rng)
right[i] = c.NewRandomZr(rng)
leftgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
rightgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
}

result1 := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Swap left and right
result2 := c.AddPairsOfProducts(right, left, rightgen, leftgen, c.GroupOrder)

assert.True(t, result1.Equals(result2), "Results should be equal when swapping left/right for curve %s", CurveIDToString(c.curveID))
})

// Test with negative scalars
t.Run("negative_scalars", func(t *testing.T) {
n := 2
left := make([]*Zr, n)
right := make([]*Zr, n)
leftgen := make([]*G1, n)
rightgen := make([]*G1, n)

for i := 0; i < n; i++ {
left[i] = c.NewRandomZr(rng)
right[i] = left[i].Copy()
right[i].Neg() // Negate to create opposite
leftgen[i] = c.GenG1.Mul(c.NewRandomZr(rng))
rightgen[i] = leftgen[i].Copy() // Use same generator
}

result := c.AddPairsOfProducts(left, right, leftgen, rightgen, c.GroupOrder)

// Result should be infinity since left[i] + (-left[i]) = 0 for same generators
assert.True(t, result.IsInfinity(), "Result should be infinity with negated scalars and same generators for curve %s", CurveIDToString(c.curveID))
})
}

// TestAddPairsOfProductsConsistency tests consistency with other operations
func TestAddPairsOfProductsConsistency(t *testing.T) {
for _, curve := range Curves {
t.Run(CurveIDToString(curve.curveID), func(t *testing.T) {
runAddPairsOfProductsConsistencyTest(t, curve)
})
}
}

func runAddPairsOfProductsConsistencyTest(t *testing.T, c *Curve) {
rng, err := c.Rand()
require.NoError(t, err)

// Test consistency with Mul2
t.Run("consistency_with_mul2", func(t *testing.T) {
a := c.NewRandomZr(rng)
b := c.NewRandomZr(rng)
P := c.GenG1.Mul(c.NewRandomZr(rng))
Q := c.GenG1.Mul(c.NewRandomZr(rng))

// Using Mul2
result1 := P.Mul2(a, Q, b)

// Using AddPairsOfProducts with single pair
result2 := c.AddPairsOfProducts([]*Zr{a}, []*Zr{b}, []*G1{P}, []*G1{Q}, c.GroupOrder)

assert.True(t, result1.Equals(result2), "AddPairsOfProducts should match Mul2 for single pair for curve %s", CurveIDToString(c.curveID))
})

// Test consistency with MultiScalarMul
t.Run("consistency_with_multiscalarmul", func(t *testing.T) {
// Skip for BLS12_381_GURVY and BLS12_381_BBS_GURVY as they have a known limitation
// with JointScalarMultiplication when using the same generator for left and right
if c.curveID == BLS12_381_GURVY || c.curveID == BLS12_381_BBS_GURVY {
t.Skip("Skipping for GURVY curves due to JointScalarMultiplication limitation with same generators")
}

n := 5
scalars := make([]*Zr, n)
scalars2 := make([]*Zr, n)
generators := make([]*G1, n)

for i := 0; i < n; i++ {
scalars[i] = c.NewRandomZr(rng)
scalars2[i] = c.NewRandomZr(rng)
generators[i] = c.GenG1.Mul(c.NewRandomZr(rng))
}

// Compute sum of scalars[i] * generators[i] + scalars2[i] * generators[i]
// This equals sum of (scalars[i] + scalars2[i]) * generators[i]
combinedScalars := make([]*Zr, n)
for i := 0; i < n; i++ {
combinedScalars[i] = c.ModAdd(scalars[i], scalars2[i], c.GroupOrder)
}

// Using MultiScalarMul
result1 := c.MultiScalarMul(generators, combinedScalars)

// Using AddPairsOfProducts
result2 := c.AddPairsOfProducts(scalars, scalars2, generators, generators, c.GroupOrder)

assert.True(t, result1.Equals(result2), "AddPairsOfProducts should match MultiScalarMul for curve %s", CurveIDToString(c.curveID))
})
}
8 changes: 8 additions & 0 deletions driver/common/curve.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ func (p *CurveBase) ModAddMul(a1 []driver.Zr, b1 []driver.Zr, modulo driver.Zr)
func (p *CurveBase) ModAddMul2(a1 driver.Zr, c1 driver.Zr, b1 driver.Zr, c2 driver.Zr, m driver.Zr) driver.Zr {
return p.ModAdd(p.ModMul(a1, c1, m), p.ModMul(b1, c2, m), m)
}

func (p *CurveBase) AddPairsOfProducts(left []driver.Zr, right []driver.Zr, leftgen []driver.G1, rightgen []driver.G1) driver.G1 {
sum := leftgen[0].Mul2(left[0], rightgen[0], right[0])
for i := 1; i < len(left); i++ {
sum.Add(leftgen[i].Mul2(left[i], rightgen[i], right[i]))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function will panic if lengths are mismatched... @adecaro what do you think? We obviously can't return an error, so panic is right, how about a length check and a panic if it fails? Or are you concerned about performance?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an if that panic with a better message is totally fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just noticed that we don't have a check on ModAddMul though.

@HayimShaul HayimShaul Feb 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so what's the conclusion?
add the check in AddPairsOfProducts and ModAddMul? or no need to check in both?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote in favour of adding length checks and panic if they fail in all implementations of all vectored implementations

}
return sum
}
29 changes: 28 additions & 1 deletion driver/gurvy/bls12381/bls12-381.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,27 @@ func (c *Curve) ModAddMul(a1, b1 []driver.Zr, m driver.Zr) driver.Zr {
return res
}

func (p *Curve) AddPairsOfProducts(left []driver.Zr, right []driver.Zr, leftgen []driver.G1, rightgen []driver.G1) driver.G1 {
tmpJac := G1Jacs.Get()
sum := G1Jacs.Get()
defer G1Jacs.Put(tmpJac)
defer G1Jacs.Put(sum)
result := &G1{}

for i := range left {
tmpJac = JointScalarMultiplication(tmpJac, &leftgen[i].(*G1).G1Affine, &rightgen[i].(*G1).G1Affine, &left[i].(*Zr).Int, &right[i].(*Zr).Int)

if i == 0 {
sum.Set(tmpJac)
} else {
sum.AddAssign(tmpJac)
}
}
result.G1Affine.FromJacobian(sum)

return result
}

func (c *Curve) ModAddMul2(a1 driver.Zr, c1 driver.Zr, b1 driver.Zr, c2 driver.Zr, m driver.Zr) driver.Zr {
aFr := frElements.Get()
defer frElements.Put(aFr)
Expand Down Expand Up @@ -779,7 +800,13 @@ func JointScalarMultiplication(p *bls12381.G1Jac, a1, a2 *bls12381.G1Affine, s1,
for i := hiWordIndex; i >= 0; i-- {
mask := uint64(3) << 62
for j := 0; j < 32; j++ {
res.Double(&res).Double(&res)
// When j == 0 res is infinity, so Double doesn't have an effect.
// We could check that res == g1Infinity
// but that's a bit less safe because the performace
// could appear to depend on the result.
if j > 0 {
res.Double(&res).Double(&res)
}
b1 := (s[0][i] & mask) >> (62 - 2*j)
b2 := (s[1][i] & mask) >> (62 - 2*j)
if b1|b2 != 0 {
Expand Down
1 change: 1 addition & 0 deletions driver/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Curve interface {
ModAddMul(driver []Zr, driver2 []Zr, zr Zr) Zr
ModAddMul2(a1 Zr, c1 Zr, b1 Zr, c2 Zr, m Zr) Zr
MultiScalarMul(a []G1, b []Zr) G1
AddPairsOfProducts(left []Zr, right []Zr, leftgen []G1, rightgen []G1) G1
}

type Zr interface {
Expand Down
14 changes: 14 additions & 0 deletions math.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,17 @@ func (c *Curve) MultiScalarMul(a []*G1, b []*Zr) *G1 {
}
return &G1{g1: c.c.MultiScalarMul(aDriver, bDriver), curveID: c.curveID}
}

func (c *Curve) AddPairsOfProducts(left []*Zr, right []*Zr, leftgen []*G1, rightgen []*G1, m *Zr) *G1 {
leftDriver := make([]driver.Zr, len(left))
rightDriver := make([]driver.Zr, len(right))
leftgenDriver := make([]driver.G1, len(leftgen))
rightgenDriver := make([]driver.G1, len(rightgen))
for i := 0; i < len(left); i++ {
leftDriver[i] = left[i].zr
rightDriver[i] = right[i].zr
leftgenDriver[i] = leftgen[i].g1
rightgenDriver[i] = rightgen[i].g1
}
return &G1{g1: c.c.AddPairsOfProducts(leftDriver, rightDriver, leftgenDriver, rightgenDriver), curveID: c.curveID}
}
1 change: 1 addition & 0 deletions math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,7 @@ func TestCurves(t *testing.T) {
runMulTest(t, curve)
runQuadDHTestPairing(t, curve)
runMultiScalarMul(t, curve)
runAddPairsOfProductsTest(t, curve)
}
}

Expand Down