Skip to content
Open
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
39 changes: 11 additions & 28 deletions token/core/zkatdlog/nogh/v1/crypto/rp/bulletproof/ipa.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ func (p *ipaProver) Prove() (*IPA, error) {
// of the left vector and right is a function of right vector.
// Both vectors are committed in com which is passed as a parameter to reduce
func (p *ipaProver) reduce(X, com *mathlib.G1) (*mathlib.Zr, *mathlib.Zr, []*mathlib.G1, []*mathlib.G1, error) {
isBLS, isBN254 := math.DispatchCurve(p.Curve)
if isBLS {
return nativeIPAReduce[bls12381fr.Element, *bls12381fr.Element](p, X, com)
} else if isBN254 {
return nativeIPAReduce[bn254fr.Element, *bn254fr.Element](p, X, com)
}

left := p.leftVector
right := p.rightVector

Expand Down Expand Up @@ -426,20 +433,8 @@ func (v *ipaVerifier) Verify(proof *IPA) error {
}

// reduceVectors reduces the size of the vectors passed in the parameters by 1/2,
// as a function of the old vectors, x and 1/x.
//
// For BLS12-381 and BN254 curves the inner loop is executed using native
// gnark-crypto field elements (nativeReduceVectors) to avoid per-element
// big.Int allocation. For all other curves the pure-mathlib path is used.
// as a function of the old vectors, x and 1/x
func reduceVectors(left, right []*mathlib.Zr, x, xInv *mathlib.Zr, c *mathlib.Curve) ([]*mathlib.Zr, []*mathlib.Zr) {
isBLS, isBN254 := math.DispatchCurve(c)
if isBLS {
return nativeReduceVectors[bls12381fr.Element, *bls12381fr.Element](left, right, x, xInv, c)
} else if isBN254 {
return nativeReduceVectors[bn254fr.Element, *bn254fr.Element](left, right, x, xInv, c)
}

// Fallback: mathlib path for unsupported curves.
l := len(left) / 2
leftPrime := make([]*mathlib.Zr, l)
rightPrime := make([]*mathlib.Zr, l)
Expand Down Expand Up @@ -525,31 +520,19 @@ func CloneGenerators(LeftGenerators, RightGenerators []*mathlib.G1) ([]*mathlib.
// sInv[i + 2^r] = sInv[i] · x_{k-1-r}^{-1} (swapped)
// sInv[i] = sInv[i] · x_{k-1-r} (swapped)
//
// For BLS12-381 and BN254 curves the inner loop is executed using native
// gnark-crypto field elements (nativeComputeSVector), which eliminates the
// big.Int allocation overhead of the mathlib.Zr wrapper on every multiply.
// For all other curves the pure-mathlib path is used as a fallback.
// This replaces the previous O(n·log n) nested-loop implementation and
// eliminates the final BatchInverse call for sInv.
//
// Input: n, challenges = [x_0, …, x_{k-1}] where n = 2^k.
// Returns (s, sInv) where sInv[i] = s[i]^{-1}.
func ComputeSVector(n int, challenges []*mathlib.Zr, curve *mathlib.Curve) ([]*mathlib.Zr, []*mathlib.Zr) {
log2n := len(challenges)

// Verify n is consistent with number of challenges.
// Verify n is consistent with number of challenges
if 1<<log2n != n {
panic("n must equal 2^(number of challenges)")
}

// Dispatch to the allocation-free native path for supported curves.
isBLS, isBN254 := math.DispatchCurve(curve)
if isBLS {
return nativeComputeSVector[bls12381fr.Element, *bls12381fr.Element](n, challenges, curve)
} else if isBN254 {
return nativeComputeSVector[bn254fr.Element, *bn254fr.Element](n, challenges, curve)
}

// Fallback: mathlib path for unsupported curves.

// Precompute challenge inverses: O(log n) with a single field inversion.
challengeInvs := math.BatchInverse(challenges, curve)

Expand Down
237 changes: 137 additions & 100 deletions token/core/zkatdlog/nogh/v1/crypto/rp/bulletproof/ipa_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,115 +8,152 @@ package bulletproof

import (
mathlib "github.com/IBM/mathlib"
math2 "github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/crypto/math"
"github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/crypto/common"
"github.com/LFDT-Panurus/panurus/token/core/zkatdlog/nogh/v1/crypto/math"
)

// nativeComputeSVector computes the s vector and its entry-wise inverse using
// native gnark-crypto field arithmetic to avoid big.Int allocations.
//
// The dual-butterfly recurrence is identical to ComputeSVector, but all
// intermediate multiplications use in-place gnark field operations (T) rather
// than mathlib.Zr wrappers around big.Int. This eliminates the O(n) heap
// allocations per butterfly round that the mathlib path incurs.
//
// The results are converted back to []*mathlib.Zr at the end so the caller
// interface is unchanged.
func nativeComputeSVector[T any, E math2.GnarkFr[T]](n int, challenges []*mathlib.Zr, curve *mathlib.Curve) ([]*mathlib.Zr, []*mathlib.Zr) {
log2n := len(challenges)

// Convert all challenges and their inverses to native field elements once.
// This costs 2·log(n) big.Int conversions total — far fewer than the O(n)
// allocations that the mathlib butterfly loop would generate.
cNative := make([]T, log2n)
cInvNative := make([]T, log2n)
for r := range log2n {
math2.SetNativeFromZr[T, E](challenges[r], E(&cNative[r]))
// Inverse of the native element directly — no big.Int round-trip needed.
E(&cInvNative[r]).Inverse(E(&cNative[r]))
}
// nativeIPAReduce performs the reduction of the inner product argument using native gnark-crypto arithmetic.
func nativeIPAReduce[T any, E math.GnarkFr[T]](p *ipaProver, X, com *mathlib.G1) (*mathlib.Zr, *mathlib.Zr, []*mathlib.G1, []*mathlib.G1, error) {
n := len(p.leftVector)

// Allocate native storage for s and sInv vectors.
sNative := make([]T, n)
sInvNative := make([]T, n)
E(&sNative[0]).SetOne()
E(&sInvNative[0]).SetOne()
for i := 1; i < n; i++ {
E(&sNative[i]).SetZero()
E(&sInvNative[i]).SetZero()
// Convert left and right vectors to native types
leftNative := make([]T, n)
rightNative := make([]T, n)
for i := range n {
math.SetNativeFromZr[T, E](p.leftVector[i], E(&leftNative[i]))
math.SetNativeFromZr[T, E](p.rightVector[i], E(&rightNative[i]))
}

// Dual butterfly: O(n) in-place multiplications with no allocation.
for r := range log2n {
halfLen := 1 << r
c := E(&cNative[log2n-1-r])
cInv := E(&cInvNative[log2n-1-r])
for i := range halfLen {
// s[i+halfLen] = s[i] * c (bit set → challenge)
E(&sNative[i+halfLen]).Mul(E(&sNative[i]), c)
// s[i] = s[i] * cInv (bit unset → inverse)
E(&sNative[i]).Mul(E(&sNative[i]), cInv)

// sInv[i+halfLen] = sInv[i] * cInv (swapped)
E(&sInvNative[i+halfLen]).Mul(E(&sInvNative[i]), cInv)
// sInv[i] = sInv[i] * c (swapped)
E(&sInvNative[i]).Mul(E(&sInvNative[i]), c)
LArray := make([]*mathlib.G1, p.NumberOfRounds)
RArray := make([]*mathlib.G1, p.NumberOfRounds)
xList := make([]*mathlib.Zr, 0, p.NumberOfRounds)

for i := range p.NumberOfRounds {
n_current := len(leftNative) / 2

// Compute leftIP and rightIP natively
var leftIPE T
E(&leftIPE).SetZero()
var rightIPE T
E(&rightIPE).SetZero()
var tmpE T
for j := range n_current {
E(&tmpE).Mul(E(&leftNative[j]), E(&rightNative[n_current+j]))
E(&leftIPE).Add(E(&leftIPE), E(&tmpE))

E(&tmpE).Mul(E(&leftNative[n_current+j]), E(&rightNative[j]))
E(&rightIPE).Add(E(&rightIPE), E(&tmpE))
}
leftIP := math.NativeToZr[T, E](E(&leftIPE), p.Curve)
rightIP := math.NativeToZr[T, E](E(&rightIPE), p.Curve)

var s, sInv []*mathlib.Zr
if i == 0 {
s = []*mathlib.Zr{math.One(p.Curve)}
sInv = []*mathlib.Zr{math.One(p.Curve)}
} else {
s, sInv = ComputeSVector(1<<i, xList, p.Curve)
}
}

// Convert back to mathlib.Zr for the caller.
s := make([]*mathlib.Zr, n)
sInv := make([]*mathlib.Zr, n)
for i := range n {
s[i] = math2.NativeToZr[T, E](E(&sNative[i]), curve)
sInv[i] = math2.NativeToZr[T, E](E(&sInvNative[i]), curve)
}
pointsL := make([]*mathlib.G1, 0, len(p.LeftGenerators)+1)
scalarsL := make([]*mathlib.Zr, 0, len(p.LeftGenerators)+1)

pointsR := make([]*mathlib.G1, 0, len(p.LeftGenerators)+1)
scalarsR := make([]*mathlib.Zr, 0, len(p.LeftGenerators)+1)

for m := range 1 << i {
var sE_, sInvE_ T
math.SetNativeFromZr[T, E](s[m], E(&sE_))
math.SetNativeFromZr[T, E](sInv[m], E(&sInvE_))
sE := E(&sE_)
sInvE := E(&sInvE_)

for j := range n_current {
idxG_R := j + (2*m+1)*n_current
idxH_L := j + 2*m*n_current

pointsL = append(pointsL, p.LeftGenerators[idxG_R], p.RightGenerators[idxH_L])
var tmp1 T
E(&tmp1).Mul(E(&leftNative[j]), sE)
var tmp2 T
E(&tmp2).Mul(E(&rightNative[n_current+j]), sInvE)
scalarsL = append(scalarsL,
math.NativeToZr[T, E](E(&tmp1), p.Curve),
math.NativeToZr[T, E](E(&tmp2), p.Curve),
)

idxG_L := j + 2*m*n_current
idxH_R := j + (2*m+1)*n_current

pointsR = append(pointsR, p.LeftGenerators[idxG_L], p.RightGenerators[idxH_R])
var tmp3 T
E(&tmp3).Mul(E(&leftNative[n_current+j]), sE)
var tmp4 T
E(&tmp4).Mul(E(&rightNative[j]), sInvE)
scalarsR = append(scalarsR,
math.NativeToZr[T, E](E(&tmp3), p.Curve),
math.NativeToZr[T, E](E(&tmp4), p.Curve),
)
}
}

return s, sInv
}
pointsL = append(pointsL, X)
scalarsL = append(scalarsL, leftIP)

// nativeReduceVectors reduces the left and right vectors by half using native
// gnark-crypto field arithmetic, eliminating the intermediate mathlib.Zr
// allocations that the mathlib path incurs per element.
//
// The recurrence is identical to reduceVectors:
//
// leftPrime[i] = left[i]*x + left[i+l]*xInv
// rightPrime[i] = right[i]*xInv + right[i+l]*x
func nativeReduceVectors[T any, E math2.GnarkFr[T]](
left, right []*mathlib.Zr,
x, xInv *mathlib.Zr,
curve *mathlib.Curve,
) ([]*mathlib.Zr, []*mathlib.Zr) {
l := len(left) / 2

// Convert x and xInv once.
var xE, xInvE T
math2.SetNativeFromZr[T, E](x, E(&xE))
math2.SetNativeFromZr[T, E](xInv, E(&xInvE))

leftPrime := make([]*mathlib.Zr, l)
rightPrime := make([]*mathlib.Zr, l)

for i := range l {
var liE, liHalfE, riE, riHalfE T
math2.SetNativeFromZr[T, E](left[i], E(&liE))
math2.SetNativeFromZr[T, E](left[i+l], E(&liHalfE))
math2.SetNativeFromZr[T, E](right[i], E(&riE))
math2.SetNativeFromZr[T, E](right[i+l], E(&riHalfE))

// leftPrime[i] = left[i]*x + left[i+l]*xInv
var tmp T
E(&liE).Mul(E(&liE), E(&xE))
E(&tmp).Mul(E(&liHalfE), E(&xInvE))
E(&liE).Add(E(&liE), E(&tmp))
leftPrime[i] = math2.NativeToZr[T, E](E(&liE), curve)

// rightPrime[i] = right[i]*xInv + right[i+l]*x
E(&riE).Mul(E(&riE), E(&xInvE))
E(&tmp).Mul(E(&riHalfE), E(&xE))
E(&riE).Add(E(&riE), E(&tmp))
rightPrime[i] = math2.NativeToZr[T, E](E(&riE), curve)
pointsR = append(pointsR, X)
scalarsR = append(scalarsR, rightIP)

LArray[i] = p.Curve.MultiScalarMul(pointsL, scalarsL)
RArray[i] = p.Curve.MultiScalarMul(pointsR, scalarsR)

array := common.GetG1Array([]*mathlib.G1{LArray[i], RArray[i]})
bytesToHash, err := array.Bytes()
if err != nil {
return nil, nil, nil, nil, err
}
x := p.Curve.HashToZr(bytesToHash)
xList = append(xList, x)

var xE_ T
math.SetNativeFromZr[T, E](x, E(&xE_))
xE := E(&xE_)
var xInvE T
E(&xInvE).Inverse(xE)

// Reduce left and right vectors natively
newLeftNative := make([]T, n_current)
newRightNative := make([]T, n_current)
for j := range n_current {
var l1 T
E(&l1).Mul(E(&leftNative[j]), xE)
var l2 T
E(&l2).Mul(E(&leftNative[n_current+j]), E(&xInvE))
E(&newLeftNative[j]).Add(E(&l1), E(&l2))

var r1 T
E(&r1).Mul(E(&rightNative[j]), E(&xInvE))
var r2 T
E(&r2).Mul(E(&rightNative[n_current+j]), xE)
E(&newRightNative[j]).Add(E(&r1), E(&r2))
}
leftNative = newLeftNative
rightNative = newRightNative

var xSquareE T
E(&xSquareE).Mul(xE, xE)
xSquare := math.NativeToZr[T, E](E(&xSquareE), p.Curve)

var xSquareInvE T
E(&xSquareInvE).Inverse(E(&xSquareE))
xSquareInv := math.NativeToZr[T, E](E(&xSquareInvE), p.Curve)

CPrime := LArray[i].Mul2(xSquare, RArray[i], xSquareInv)
CPrime.Add(com)
com = CPrime
}

return leftPrime, rightPrime
leftResult := math.NativeToZr[T, E](E(&leftNative[0]), p.Curve)
rightResult := math.NativeToZr[T, E](E(&rightNative[0]), p.Curve)

return leftResult, rightResult, LArray, RArray, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,59 +198,6 @@ func TestNativeRPTamperedProofRejected(t *testing.T) {
bad.Data.T2 = ctx.curve.GenG1.Mul(ctx.curve.NewRandomZr(rand))
assert.Error(t, ctx.verify(t, bad))
})

t.Run("tampered_C", func(t *testing.T) {
bad := copyRangeProof(t, proof)
bad.Data.C = ctx.curve.GenG1.Mul(ctx.curve.NewRandomZr(rand))
assert.Error(t, ctx.verify(t, bad))
})

t.Run("tampered_D", func(t *testing.T) {
bad := copyRangeProof(t, proof)
bad.Data.D = ctx.curve.GenG1.Mul(ctx.curve.NewRandomZr(rand))
assert.Error(t, ctx.verify(t, bad))
})
})
}
}

// TestNativeRPForeignCommitmentsRejected is F-09 (zkatdlog security report): "Missing
// Domain Separation in Bulletproof z-Challenge Derivation". The report observes that
// y := HashToZr(C, D, V) but z := HashToZr(y.Bytes()) does not re-hash C, D, V directly,
// and argues this violates the Fiat-Shamir requirement that every challenge be bound to
// all prior commitments, concluding z is "not bound to the original proof commitments".
//
// This is refuted. Two independent reasons z remains bound to (C, D, V), either of which
// is sufficient on its own:
//
// 1. z = H(y) with y = H(C, D, V) is a one-way hash chain, not an unbound value. Under
// the random-oracle model a prover cannot influence z without going through y, and
// cannot influence y without going through (C, D, V) — so z is bound transitively,
// not directly. This is the same "chained transcript" pattern used elsewhere for
// Fiat-Shamir composition and does not by itself weaken soundness.
// 2. Independently of how z is derived, C and D are not merely hashed for the
// challenge — they are used as commitment points in the algebraic equation Verify
// checks (see rp.go Verify/verifyIPA and rp_native.go nativeRPVerify/
// nativeRPVerifyIPA: both D and C appear directly in the MultiScalarMul that must
// equal the IPA commitment). So substituting foreign C/D values breaks the checked
// equation regardless of what z would have hashed to.
//
// This test swaps in a genuine (C, D) pair taken from an entirely different, honestly
// generated proof for the same commitment/value — the scenario the report's "not bound"
// language would predict succeeds if z's binding to C/D were actually missing. It is
// rejected, confirming the finding does not translate into an exploitable forgery.
func TestNativeRPForeignCommitmentsRejected(t *testing.T) {
for _, tc := range nativeCurves() {
t.Run(tc.name, func(t *testing.T) {
ctx := newNativeTestCtx(t, tc.id, 32, 100)
proof := ctx.prove(t, 100)
foreign := ctx.prove(t, 100)

bad := copyRangeProof(t, proof)
bad.Data.C = foreign.Data.C
bad.Data.D = foreign.Data.D
err := ctx.verify(t, bad)
require.Error(t, err, "F-09: proof with foreign C/D from another honest proof must be rejected")
})
}
}
Expand Down
Loading