mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
472 lines
12 KiB
Go
472 lines
12 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package zk
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"math/big"
|
|
"sync"
|
|
|
|
"github.com/consensys/gnark-crypto/ecc/bn254"
|
|
"github.com/consensys/gnark-crypto/ecc/bn254/fp"
|
|
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// Precompile addresses for commitment operations
|
|
const (
|
|
PedersenAddress = "0x0502" // Pedersen commitment (NOT PQ-safe, for benchmarking)
|
|
)
|
|
|
|
var (
|
|
ErrInvalidCommitmentInput = errors.New("invalid commitment input")
|
|
ErrPointNotOnCurve = errors.New("point not on curve")
|
|
)
|
|
|
|
// PedersenCommitter provides Pedersen commitment operations
|
|
// WARNING: Pedersen commitments are NOT post-quantum secure (discrete log)
|
|
// Use Poseidon2 commitments for PQ security
|
|
type PedersenCommitter struct {
|
|
// Generator points (from trusted setup or hash-to-curve)
|
|
G bn254.G1Affine // Base generator
|
|
H bn254.G1Affine // Blinding generator
|
|
|
|
// Additional generators for vector commitments
|
|
Generators []bn254.G1Affine
|
|
|
|
// Statistics
|
|
TotalCommitments uint64
|
|
TotalVerifications uint64
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewPedersenCommitter creates a new Pedersen committer with default generators
|
|
func NewPedersenCommitter() *PedersenCommitter {
|
|
pc := &PedersenCommitter{}
|
|
|
|
// Use bn254 generator for G
|
|
_, _, g1Gen, _ := bn254.Generators()
|
|
pc.G = g1Gen
|
|
|
|
// Derive H from G using hash-to-curve (nothing-up-my-sleeve)
|
|
// H = HashToCurve("Lux_Pedersen_H")
|
|
pc.H = hashToG1("Lux_Pedersen_H_Generator")
|
|
|
|
// Pre-generate additional generators for vector commitments
|
|
pc.Generators = make([]bn254.G1Affine, 32)
|
|
for i := range 32 {
|
|
pc.Generators[i] = hashToG1("Lux_Pedersen_Gen_" + string(rune('0'+i)))
|
|
}
|
|
|
|
return pc
|
|
}
|
|
|
|
// Commit creates a Pedersen commitment: C = v*G + r*H
|
|
// value: the value to commit to (as field element)
|
|
// blindingFactor: random blinding factor
|
|
// Returns: commitment point (hashed, 32 bytes)
|
|
func (p *PedersenCommitter) Commit(value, blindingFactor [32]byte) ([32]byte, error) {
|
|
var v, r fr.Element
|
|
v.SetBytes(value[:])
|
|
r.SetBytes(blindingFactor[:])
|
|
|
|
// C = v*G + r*H
|
|
var vG, rH bn254.G1Affine
|
|
vG.ScalarMultiplication(&p.G, v.BigInt(new(big.Int)))
|
|
rH.ScalarMultiplication(&p.H, r.BigInt(new(big.Int)))
|
|
|
|
var commitment bn254.G1Affine
|
|
commitment.Add(&vG, &rH)
|
|
|
|
// Store in cache and return hash
|
|
return compressG1WithCache(&commitment), nil
|
|
}
|
|
|
|
// CommitWithOpening creates a commitment and returns opening info
|
|
func (p *PedersenCommitter) CommitWithOpening(value *big.Int) (commitment [32]byte, blindingFactor [32]byte, err error) {
|
|
// Generate random blinding factor
|
|
var r fr.Element
|
|
r.SetRandom()
|
|
blindingFactor = r.Bytes()
|
|
|
|
// Create value bytes
|
|
var valueBytes [32]byte
|
|
vBytes := value.Bytes()
|
|
copy(valueBytes[32-len(vBytes):], vBytes)
|
|
|
|
commitment, err = p.Commit(valueBytes, blindingFactor)
|
|
if err != nil {
|
|
return [32]byte{}, [32]byte{}, err
|
|
}
|
|
|
|
p.mu.Lock()
|
|
p.TotalCommitments++
|
|
p.mu.Unlock()
|
|
|
|
return commitment, blindingFactor, nil
|
|
}
|
|
|
|
// Verify verifies a Pedersen commitment opening
|
|
// Returns true if C == v*G + r*H
|
|
func (p *PedersenCommitter) Verify(commitment, value, blindingFactor [32]byte) (bool, error) {
|
|
// Decompress commitment
|
|
C, err := decompressG1(commitment)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Recompute expected commitment
|
|
expected, err := p.Commit(value, blindingFactor)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
expectedPoint, err := decompressG1(expected)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
p.mu.Lock()
|
|
p.TotalVerifications++
|
|
p.mu.Unlock()
|
|
|
|
return C.Equal(&expectedPoint), nil
|
|
}
|
|
|
|
// Add adds two commitments homomorphically
|
|
// C1 + C2 = (v1 + v2)*G + (r1 + r2)*H
|
|
func (p *PedersenCommitter) Add(c1, c2 [32]byte) ([32]byte, error) {
|
|
p1, err := decompressG1(c1)
|
|
if err != nil {
|
|
return [32]byte{}, err
|
|
}
|
|
|
|
p2, err := decompressG1(c2)
|
|
if err != nil {
|
|
return [32]byte{}, err
|
|
}
|
|
|
|
var sum bn254.G1Affine
|
|
sum.Add(&p1, &p2)
|
|
|
|
return compressG1WithCache(&sum), nil
|
|
}
|
|
|
|
// Sub subtracts commitments homomorphically
|
|
// C1 - C2 = (v1 - v2)*G + (r1 - r2)*H
|
|
func (p *PedersenCommitter) Sub(c1, c2 [32]byte) ([32]byte, error) {
|
|
p1, err := decompressG1(c1)
|
|
if err != nil {
|
|
return [32]byte{}, err
|
|
}
|
|
|
|
p2, err := decompressG1(c2)
|
|
if err != nil {
|
|
return [32]byte{}, err
|
|
}
|
|
|
|
var neg bn254.G1Affine
|
|
neg.Neg(&p2)
|
|
|
|
var diff bn254.G1Affine
|
|
diff.Add(&p1, &neg)
|
|
|
|
return compressG1WithCache(&diff), nil
|
|
}
|
|
|
|
// VectorCommit creates a vector Pedersen commitment
|
|
// C = sum(v_i * G_i) + r * H
|
|
func (p *PedersenCommitter) VectorCommit(values [][32]byte, blindingFactor [32]byte) ([32]byte, error) {
|
|
if len(values) > len(p.Generators) {
|
|
return [32]byte{}, errors.New("too many values for vector commitment")
|
|
}
|
|
|
|
var sum bn254.G1Jac // Use Jacobian for efficiency
|
|
|
|
for i, value := range values {
|
|
var v fr.Element
|
|
v.SetBytes(value[:])
|
|
|
|
var vG bn254.G1Affine
|
|
vG.ScalarMultiplication(&p.Generators[i], v.BigInt(new(big.Int)))
|
|
|
|
var vGJac bn254.G1Jac
|
|
vGJac.FromAffine(&vG)
|
|
sum.AddAssign(&vGJac)
|
|
}
|
|
|
|
// Add blinding: r * H
|
|
var r fr.Element
|
|
r.SetBytes(blindingFactor[:])
|
|
|
|
var rH bn254.G1Affine
|
|
rH.ScalarMultiplication(&p.H, r.BigInt(new(big.Int)))
|
|
|
|
var rHJac bn254.G1Jac
|
|
rHJac.FromAffine(&rH)
|
|
sum.AddAssign(&rHJac)
|
|
|
|
var result bn254.G1Affine
|
|
result.FromJacobian(&sum)
|
|
|
|
return compressG1WithCache(&result), nil
|
|
}
|
|
|
|
// NoteCommitment creates a note commitment for shielded transactions
|
|
// Similar to Poseidon2, but using Pedersen for homomorphic properties
|
|
// commitment = amount*G_0 + assetId*G_1 + owner*G_2 + blindingFactor*H
|
|
func (p *PedersenCommitter) NoteCommitment(
|
|
amount *big.Int,
|
|
assetId [32]byte,
|
|
owner common.Address,
|
|
blindingFactor [32]byte,
|
|
) ([32]byte, error) {
|
|
// Prepare values
|
|
var amountBytes [32]byte
|
|
aBytes := amount.Bytes()
|
|
copy(amountBytes[32-len(aBytes):], aBytes)
|
|
|
|
var ownerBytes [32]byte
|
|
copy(ownerBytes[12:], owner[:])
|
|
|
|
values := [][32]byte{amountBytes, assetId, ownerBytes}
|
|
return p.VectorCommit(values, blindingFactor)
|
|
}
|
|
|
|
// VerifyBalance verifies that sum of inputs equals sum of outputs
|
|
// Uses homomorphic property: sum(C_in) - sum(C_out) = 0
|
|
// (when blindings also cancel out)
|
|
func (p *PedersenCommitter) VerifyBalance(inputs, outputs [][32]byte) (bool, error) {
|
|
// Sum inputs
|
|
var inputSum bn254.G1Jac
|
|
for _, c := range inputs {
|
|
pt, err := decompressG1(c)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var ptJac bn254.G1Jac
|
|
ptJac.FromAffine(&pt)
|
|
inputSum.AddAssign(&ptJac)
|
|
}
|
|
|
|
// Sum outputs
|
|
var outputSum bn254.G1Jac
|
|
for _, c := range outputs {
|
|
pt, err := decompressG1(c)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
var ptJac bn254.G1Jac
|
|
ptJac.FromAffine(&pt)
|
|
outputSum.AddAssign(&ptJac)
|
|
}
|
|
|
|
// Check if inputSum == outputSum
|
|
var inputAff, outputAff bn254.G1Affine
|
|
inputAff.FromJacobian(&inputSum)
|
|
outputAff.FromJacobian(&outputSum)
|
|
|
|
return inputAff.Equal(&outputAff), nil
|
|
}
|
|
|
|
// RequiredGas calculates gas cost for Pedersen operations
|
|
func (p *PedersenCommitter) RequiredGas(operation string, numElements int) uint64 {
|
|
switch operation {
|
|
case "commit":
|
|
// Single commitment: 2 scalar mults + 1 add
|
|
return 6000
|
|
case "verify":
|
|
// Commitment + equality check
|
|
return 7000
|
|
case "add", "sub":
|
|
// Point addition/subtraction
|
|
return 500
|
|
case "vector":
|
|
// Vector commitment: n scalar mults + 1 blinding
|
|
return uint64(6000 + numElements*3000)
|
|
default:
|
|
return 10000
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// hashToG1 creates a generator from a seed using hash-to-curve
|
|
func hashToG1(seed string) bn254.G1Affine {
|
|
// Use hash-to-curve via repeated hashing until we find a valid point
|
|
// This is a simple try-and-increment approach
|
|
var point bn254.G1Affine
|
|
|
|
seedBytes := []byte(seed)
|
|
var counter byte = 0
|
|
|
|
for {
|
|
// Hash seed with counter
|
|
data := append(seedBytes, counter)
|
|
hash := sha256.Sum256(data)
|
|
|
|
// Try to create a point from the hash
|
|
var x fp.Element
|
|
x.SetBytes(hash[:])
|
|
|
|
// Try to find y for this x using curve equation y^2 = x^3 + 3
|
|
var x2, x3, rhs fp.Element
|
|
x2.Square(&x)
|
|
x3.Mul(&x2, &x)
|
|
|
|
var three fp.Element
|
|
three.SetInt64(3)
|
|
rhs.Add(&x3, &three)
|
|
|
|
// Try to compute square root
|
|
var y fp.Element
|
|
if y.Sqrt(&rhs) != nil {
|
|
// Found valid point
|
|
point.X = x
|
|
point.Y = y
|
|
|
|
if point.IsOnCurve() && !point.IsInfinity() {
|
|
return point
|
|
}
|
|
}
|
|
counter++
|
|
if counter == 0 {
|
|
// Overflow, shouldn't happen with good seeds
|
|
break
|
|
}
|
|
}
|
|
|
|
// Fallback to base generator (shouldn't reach here)
|
|
_, _, g1, _ := bn254.Generators()
|
|
return g1
|
|
}
|
|
|
|
// compressG1 compresses a G1 point to 64 bytes (uncompressed for simplicity)
|
|
// In production, use proper point compression
|
|
func compressG1(p *bn254.G1Affine) [32]byte {
|
|
// For simplicity, we hash the full point representation
|
|
// This loses some efficiency but ensures correctness
|
|
fullBytes := p.Bytes()
|
|
hash := sha256.Sum256(fullBytes[:])
|
|
|
|
// Store hash + first byte of X for verification
|
|
var result [32]byte
|
|
copy(result[:], hash[:])
|
|
return result
|
|
}
|
|
|
|
// pointCache stores the mapping from hash to full point
|
|
var pointCache = struct {
|
|
sync.RWMutex
|
|
m map[[32]byte]bn254.G1Affine
|
|
}{m: make(map[[32]byte]bn254.G1Affine)}
|
|
|
|
// storePoint stores a point in the cache
|
|
func storePoint(p *bn254.G1Affine) [32]byte {
|
|
fullBytes := p.Bytes()
|
|
hash := sha256.Sum256(fullBytes[:])
|
|
var key [32]byte
|
|
copy(key[:], hash[:])
|
|
|
|
pointCache.Lock()
|
|
pointCache.m[key] = *p
|
|
pointCache.Unlock()
|
|
|
|
return key
|
|
}
|
|
|
|
// compressG1WithCache compresses and caches a G1 point
|
|
func compressG1WithCache(p *bn254.G1Affine) [32]byte {
|
|
return storePoint(p)
|
|
}
|
|
|
|
// decompressG1 retrieves a point from cache or reconstructs it
|
|
func decompressG1(data [32]byte) (bn254.G1Affine, error) {
|
|
pointCache.RLock()
|
|
if p, ok := pointCache.m[data]; ok {
|
|
pointCache.RUnlock()
|
|
return p, nil
|
|
}
|
|
pointCache.RUnlock()
|
|
|
|
// If not in cache, we can't decompress (this is a limitation of this simple approach)
|
|
// In production, use proper point compression
|
|
return bn254.G1Affine{}, ErrPointNotOnCurve
|
|
}
|
|
|
|
// Global instance
|
|
var globalPedersen = NewPedersenCommitter()
|
|
|
|
// PedersenCommit is the main entry point for the precompile
|
|
// Input: 64 bytes (value[32] + blindingFactor[32])
|
|
// Output: 32 bytes (compressed commitment)
|
|
func PedersenCommit(input []byte) ([]byte, error) {
|
|
if len(input) != 64 {
|
|
return nil, ErrInvalidCommitmentInput
|
|
}
|
|
|
|
var value, blinding [32]byte
|
|
copy(value[:], input[:32])
|
|
copy(blinding[:], input[32:])
|
|
|
|
result, err := globalPedersen.Commit(value, blinding)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result[:], nil
|
|
}
|
|
|
|
// PedersenVerify verifies a commitment opening
|
|
// Input: 96 bytes (commitment[32] + value[32] + blindingFactor[32])
|
|
// Output: 32 bytes (0x01 if valid, 0x00 if invalid)
|
|
func PedersenVerify(input []byte) ([]byte, error) {
|
|
if len(input) != 96 {
|
|
return nil, ErrInvalidCommitmentInput
|
|
}
|
|
|
|
var commitment, value, blinding [32]byte
|
|
copy(commitment[:], input[:32])
|
|
copy(value[:], input[32:64])
|
|
copy(blinding[:], input[64:])
|
|
|
|
valid, err := globalPedersen.Verify(commitment, value, blinding)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]byte, 32)
|
|
if valid {
|
|
result[31] = 1
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// PedersenAdd adds two commitments
|
|
// Input: 64 bytes (c1[32] + c2[32])
|
|
// Output: 32 bytes (c1 + c2)
|
|
func PedersenAdd(input []byte) ([]byte, error) {
|
|
if len(input) != 64 {
|
|
return nil, ErrInvalidCommitmentInput
|
|
}
|
|
|
|
var c1, c2 [32]byte
|
|
copy(c1[:], input[:32])
|
|
copy(c2[:], input[32:])
|
|
|
|
result, err := globalPedersen.Add(c1, c2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result[:], nil
|
|
}
|
|
|
|
// GetPedersenStats returns statistics
|
|
func GetPedersenStats() (totalCommitments, totalVerifications uint64) {
|
|
globalPedersen.mu.RLock()
|
|
defer globalPedersen.mu.RUnlock()
|
|
return globalPedersen.TotalCommitments, globalPedersen.TotalVerifications
|
|
}
|