mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
pedersen: add deterministic NewGeneratorsFromSeed for cross-language KATs
NewGeneratorsFromSeed(seed [32]byte) derives (G, H) deterministically via
RFC 9380 hash-to-curve (SVDW) on BN254 G1, with msg = seed || u64_le(index)
and DST = "LUX_PEDERSEN_SEEDED_GEN_V1". BN254 G1 has cofactor 1, so outputs
are subgroup-correct without clearing.
Existing crypto/rand-backed NewGenerators is unchanged.
Tests: 5 same-seed determinism cases, 5 cross-seed isolation cases, 1
homomorphism check on the seeded basis, 1 frozen golden vector for the
incrementing seed {0..31} so future C++/Rust KAT generators can be cross-
checked against Go byte-for-byte.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pedersen
|
||||
|
||||
import (
|
||||
bn254 "github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
)
|
||||
|
||||
// SeededGenDST is the domain separation tag used by NewGeneratorsFromSeed.
|
||||
// It is exported so cross-language KAT generators (C++, Rust) can replicate
|
||||
// the derivation byte-for-byte.
|
||||
const SeededGenDST = "LUX_PEDERSEN_SEEDED_GEN_V1"
|
||||
|
||||
// NewGeneratorsFromSeed deterministically derives the Pedersen generators
|
||||
// (G, H) from a 32-byte seed using RFC 9380 hash-to-curve (SVDW map) on
|
||||
// BN254 G1.
|
||||
//
|
||||
// Derivation algorithm (replicate exactly in C++/Rust for KAT parity):
|
||||
//
|
||||
// 1. msg_i = seed (32 bytes) || u64_le(i) for i in {0, 1}
|
||||
// 2. P_i = bn254.HashToG1(msg_i, dst=SeededGenDST)
|
||||
// 3. G = P_0, H = P_1
|
||||
//
|
||||
// where u64_le(i) is the 8-byte little-endian encoding of i. Because BN254
|
||||
// G1 has cofactor 1, every output of HashToG1 is in the prime-order subgroup,
|
||||
// so no clearing is needed.
|
||||
//
|
||||
// Same seed → identical (G, H) on every machine, every run.
|
||||
//
|
||||
// Use this constructor for cross-language Known-Answer-Test (KAT) generation
|
||||
// or for any setting that requires reproducibility. For production where a
|
||||
// fresh, non-reproducible basis is desired, use NewGenerators(nil) instead.
|
||||
func NewGeneratorsFromSeed(seed [32]byte) (*Generators, error) {
|
||||
g, err := hashIndexedG1(seed, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := hashIndexedG1(seed, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if g.Equal(&h) {
|
||||
return nil, ErrIdenticalGenerators
|
||||
}
|
||||
gen := &Generators{}
|
||||
gen.G.FromAffine(&g)
|
||||
gen.H.FromAffine(&h)
|
||||
return gen, nil
|
||||
}
|
||||
|
||||
// hashIndexedG1 returns HashToG1(seed || u64_le(index), dst=SeededGenDST).
|
||||
func hashIndexedG1(seed [32]byte, index uint64) (bn254.G1Affine, error) {
|
||||
var msg [40]byte
|
||||
copy(msg[:32], seed[:])
|
||||
// little-endian u64
|
||||
for i := uint64(0); i < 8; i++ {
|
||||
msg[32+i] = byte(index >> (8 * i))
|
||||
}
|
||||
return bn254.HashToG1(msg[:], []byte(SeededGenDST))
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pedersen
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
bn254 "github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
)
|
||||
|
||||
func affineHex(p *bn254.G1Affine) string {
|
||||
b := p.Bytes() // gnark-crypto compressed form: 32 bytes (X with Y-parity flags)
|
||||
return hex.EncodeToString(b[:])
|
||||
}
|
||||
|
||||
func toAffine(g *Generators) (bn254.G1Affine, bn254.G1Affine) {
|
||||
var gA, hA bn254.G1Affine
|
||||
gA.FromJacobian(&g.G)
|
||||
hA.FromJacobian(&g.H)
|
||||
return gA, hA
|
||||
}
|
||||
|
||||
// TestNewGeneratorsFromSeed_Deterministic: same seed → same generators.
|
||||
func TestNewGeneratorsFromSeed_Deterministic(t *testing.T) {
|
||||
cases := [][32]byte{
|
||||
zeroSeed(),
|
||||
oneSeed(),
|
||||
incrementingSeed(),
|
||||
highBitSeed(),
|
||||
mixedSeed(),
|
||||
}
|
||||
for i, seed := range cases {
|
||||
a, err := NewGeneratorsFromSeed(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: a: %v", i, err)
|
||||
}
|
||||
b, err := NewGeneratorsFromSeed(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: b: %v", i, err)
|
||||
}
|
||||
ag, ah := toAffine(a)
|
||||
bg, bh := toAffine(b)
|
||||
if !ag.Equal(&bg) || !ah.Equal(&bh) {
|
||||
t.Fatalf("case %d: non-deterministic output for identical seed", i)
|
||||
}
|
||||
if !ag.IsInSubGroup() || !ah.IsInSubGroup() {
|
||||
t.Fatalf("case %d: derived generator not in prime-order subgroup", i)
|
||||
}
|
||||
if ag.Equal(&ah) {
|
||||
t.Fatalf("case %d: G and H must be independent", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGeneratorsFromSeed_DistinctSeeds: different seeds → different generators.
|
||||
func TestNewGeneratorsFromSeed_DistinctSeeds(t *testing.T) {
|
||||
pairs := [][2][32]byte{
|
||||
{zeroSeed(), oneSeed()},
|
||||
{zeroSeed(), incrementingSeed()},
|
||||
{oneSeed(), highBitSeed()},
|
||||
{incrementingSeed(), mixedSeed()},
|
||||
{highBitSeed(), mixedSeed()},
|
||||
}
|
||||
for i, p := range pairs {
|
||||
a, err := NewGeneratorsFromSeed(p[0])
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: a: %v", i, err)
|
||||
}
|
||||
b, err := NewGeneratorsFromSeed(p[1])
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: b: %v", i, err)
|
||||
}
|
||||
ag, ah := toAffine(a)
|
||||
bg, bh := toAffine(b)
|
||||
if ag.Equal(&bg) {
|
||||
t.Fatalf("case %d: distinct seeds produced identical G", i)
|
||||
}
|
||||
if ah.Equal(&bh) {
|
||||
t.Fatalf("case %d: distinct seeds produced identical H", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGeneratorsFromSeed_CommitsHomomorphic: derived basis still satisfies
|
||||
// the Pedersen homomorphism: Commit(m1+m2, r1+r2) == Commit(m1, r1) + Commit(m2, r2).
|
||||
func TestNewGeneratorsFromSeed_CommitsHomomorphic(t *testing.T) {
|
||||
gens, err := NewGeneratorsFromSeed(incrementingSeed())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m1, r1, m2, r2 fr.Element
|
||||
m1.SetUint64(7)
|
||||
r1.SetUint64(13)
|
||||
m2.SetUint64(5)
|
||||
r2.SetUint64(11)
|
||||
|
||||
c1 := gens.Commit(&m1, &r1)
|
||||
c2 := gens.Commit(&m2, &r2)
|
||||
|
||||
var mSum, rSum fr.Element
|
||||
mSum.Add(&m1, &m2)
|
||||
rSum.Add(&r1, &r2)
|
||||
cSum := gens.Commit(&mSum, &rSum)
|
||||
|
||||
var aJ, bJ bn254.G1Jac
|
||||
aJ.FromAffine(&c1)
|
||||
bJ.FromAffine(&c2)
|
||||
aJ.AddAssign(&bJ)
|
||||
var got bn254.G1Affine
|
||||
got.FromJacobian(&aJ)
|
||||
|
||||
if !got.Equal(&cSum) {
|
||||
t.Fatalf("homomorphism violated for seeded generators")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGeneratorsFromSeed_GoldenVector freezes a hard-coded expected output
|
||||
// for seed = {0,1,...,31}. Any future change to the derivation algorithm will
|
||||
// trip this test, forcing a coordinated update to the C++/Rust KAT generators.
|
||||
//
|
||||
// Encoding: gnark-crypto bn254.G1Affine.Bytes() is 32 bytes, compressed
|
||||
// big-endian (X coord with Y-parity in top bits, per the gnark-crypto
|
||||
// internal compressed form).
|
||||
func TestNewGeneratorsFromSeed_GoldenVector(t *testing.T) {
|
||||
gens, err := NewGeneratorsFromSeed(incrementingSeed())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gA, hA := toAffine(gens)
|
||||
gotG := affineHex(&gA)
|
||||
gotH := affineHex(&hA)
|
||||
|
||||
const wantG = "afba7c7a97100c5eb0ec96758698779b5d8d38d228bcdb7c85a4c1626ea5247a"
|
||||
const wantH = "abc19b5bad508d8e7b944a37812a342cdbaa5946f0b3fd854805820c006c6110"
|
||||
|
||||
if gotG != wantG {
|
||||
t.Fatalf("golden G mismatch (derivation regression):\n got=%s\nwant=%s", gotG, wantG)
|
||||
}
|
||||
if gotH != wantH {
|
||||
t.Fatalf("golden H mismatch (derivation regression):\n got=%s\nwant=%s", gotH, wantH)
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
func zeroSeed() [32]byte {
|
||||
var s [32]byte
|
||||
return s
|
||||
}
|
||||
|
||||
func oneSeed() [32]byte {
|
||||
var s [32]byte
|
||||
for i := range s {
|
||||
s[i] = 1
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func incrementingSeed() [32]byte {
|
||||
var s [32]byte
|
||||
for i := range s {
|
||||
s[i] = byte(i)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func highBitSeed() [32]byte {
|
||||
var s [32]byte
|
||||
for i := range s {
|
||||
s[i] = 0x80
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func mixedSeed() [32]byte {
|
||||
var s [32]byte
|
||||
for i := range s {
|
||||
s[i] = byte(0xA5 ^ i)
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user