Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bls254.gno

9.33 Kb · 295 lines
  1// Package bls254 implements BLS signature verification over BN254, plus the
  2// hash-to-G1 construction and message encoding used by the SPLASH beacon.
  3//
  4// Curve: alt_bn128 / BN254. G1: y^2 = x^3 + 3 over Fp. Points are encoded as
  5// 64 bytes (x|y), each coordinate 32-byte big-endian. G2 points (as used for
  6// the publisher's public key and the fixed generator) are 128 bytes,
  7// EIP-197 wire order: x_imag | x_real | y_imag | y_real.
  8//
  9// There is no big-integer type in Gno, so field arithmetic goes through
 10// crypto/modexp (EIP-198 modular exponentiation) plus hand-rolled 4-limb
 11// compare/subtract/add for the one modular addition hash-to-G1 needs.
 12package bls254
 13
 14import (
 15	"crypto/bn254"
 16	"crypto/modexp"
 17	"crypto/sha256"
 18	"encoding/hex"
 19	"math/bits"
 20	"strconv"
 21)
 22
 23// DOMAIN is the hash-to-G1 domain separation tag for the SPLASH beacon.
 24const DOMAIN = "SPLASH-BEACON-BN254-G1-v1"
 25
 26const (
 27	// pHex is the BN254 base field modulus, big-endian.
 28	pHex = "30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47"
 29	// sqrtExpHex is (P+1)/4, a valid modular square-root exponent because
 30	// P % 4 == 3.
 31	sqrtExpHex = "0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52"
 32	// g2GenHex is the canonical G2 generator, EIP-197 wire order
 33	// (x_imag | x_real | y_imag | y_real).
 34	g2GenHex = "198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa"
 35)
 36
 37var (
 38	pBytes       = mustDecodeHex(pHex)
 39	pLimbs       = toLimbs(pBytes)
 40	sqrtExpBytes = mustDecodeHex(sqrtExpHex)
 41	g2GenBytes   = mustDecodeHex(g2GenHex)
 42)
 43
 44func mustDecodeHex(s string) []byte {
 45	b, err := hex.DecodeString(s)
 46	if err != nil {
 47		panic("bls254: bad constant hex")
 48	}
 49	return b
 50}
 51
 52// ---- 4-limb (256-bit) field helpers. Inputs/outputs are 32-byte big-endian. ----
 53//
 54// Every function in this block is hand-unrolled over the four limbs rather than
 55// written as a `for i := 0; i < 4; i++` loop. That is not style, it is the second
 56// largest gas item in the drand path after the field multiply itself. Measured on
 57// this toolchain, 100 calls with the harness baseline subtracted:
 58//
 59//	              looped    unrolled   saved
 60//	toLimbs       52,705      39,328     25%
 61//	fromLimbs     58,961      42,308     28%
 62//	addModP       56,287      49,420     12%
 63//
 64// The gno VM charges per interpreted operation, so the loop counter, the bound
 65// test and the i*8 and 3-i index arithmetic cost real gas on top of the eight
 66// shift-ors they schedule. halveModP in fieldmul.gno was already unrolled and
 67// measured 21,035 against addModP's looped 56,287 for strictly more work, which
 68// is what prompted this.
 69//
 70// The saving is smaller than that comparison suggested -- addModP gave back 12%,
 71// not the 2.5x the halveModP contrast implied, because most of its cost is the
 72// four bits.Add64 calls and the cmp, not the loop scaffolding. The numbers above
 73// are measured on this toolchain, not predicted.
 74//
 75// The unrolled bodies are mechanical transcriptions of the loops they replace;
 76// TestLimbHelpersUnrolledMatchLooped in bls254_test.gno keeps a copy of each
 77// original loop and asserts the two agree on a large operand set, so the
 78// transcription is checked rather than eyeballed.
 79
 80func toLimbs(b []byte) [4]uint64 {
 81	var l [4]uint64
 82	l[3] = uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
 83		uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])
 84	l[2] = uint64(b[8])<<56 | uint64(b[9])<<48 | uint64(b[10])<<40 | uint64(b[11])<<32 |
 85		uint64(b[12])<<24 | uint64(b[13])<<16 | uint64(b[14])<<8 | uint64(b[15])
 86	l[1] = uint64(b[16])<<56 | uint64(b[17])<<48 | uint64(b[18])<<40 | uint64(b[19])<<32 |
 87		uint64(b[20])<<24 | uint64(b[21])<<16 | uint64(b[22])<<8 | uint64(b[23])
 88	l[0] = uint64(b[24])<<56 | uint64(b[25])<<48 | uint64(b[26])<<40 | uint64(b[27])<<32 |
 89		uint64(b[28])<<24 | uint64(b[29])<<16 | uint64(b[30])<<8 | uint64(b[31])
 90	return l
 91}
 92
 93func fromLimbs(l [4]uint64) []byte {
 94	b := make([]byte, 32)
 95	v := l[3]
 96	b[0], b[1], b[2], b[3] = byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32)
 97	b[4], b[5], b[6], b[7] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
 98	v = l[2]
 99	b[8], b[9], b[10], b[11] = byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32)
100	b[12], b[13], b[14], b[15] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
101	v = l[1]
102	b[16], b[17], b[18], b[19] = byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32)
103	b[20], b[21], b[22], b[23] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
104	v = l[0]
105	b[24], b[25], b[26], b[27] = byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32)
106	b[28], b[29], b[30], b[31] = byte(v>>24), byte(v>>16), byte(v>>8), byte(v)
107	return b
108}
109
110// cmp returns -1, 0, 1 for a<b, a==b, a>b. Most-significant limb first, so the
111// common case of a difference in the top limb returns after one comparison.
112func cmp(a, b [4]uint64) int {
113	if a[3] != b[3] {
114		if a[3] < b[3] {
115			return -1
116		}
117		return 1
118	}
119	if a[2] != b[2] {
120		if a[2] < b[2] {
121			return -1
122		}
123		return 1
124	}
125	if a[1] != b[1] {
126		if a[1] < b[1] {
127			return -1
128		}
129		return 1
130	}
131	if a[0] != b[0] {
132		if a[0] < b[0] {
133			return -1
134		}
135		return 1
136	}
137	return 0
138}
139
140func sub(a, b [4]uint64) [4]uint64 {
141	var r [4]uint64
142	var borrow uint64
143	r[0], borrow = bits.Sub64(a[0], b[0], 0)
144	r[1], borrow = bits.Sub64(a[1], b[1], borrow)
145	r[2], borrow = bits.Sub64(a[2], b[2], borrow)
146	r[3], _ = bits.Sub64(a[3], b[3], borrow)
147	return r
148}
149
150func add(a, b [4]uint64) ([4]uint64, uint64) {
151	var r [4]uint64
152	var carry uint64
153	r[0], carry = bits.Add64(a[0], b[0], 0)
154	r[1], carry = bits.Add64(a[1], b[1], carry)
155	r[2], carry = bits.Add64(a[2], b[2], carry)
156	r[3], carry = bits.Add64(a[3], b[3], carry)
157	return r, carry
158}
159
160// addModP computes (a + b) mod P for a, b already reduced mod P.
161func addModP(a, b, p [4]uint64) [4]uint64 {
162	s, carry := add(a, b)
163	if carry != 0 || cmp(s, p) >= 0 {
164		s = sub(s, p)
165	}
166	return s
167}
168
169// ---- hash-to-G1 ----
170
171// hashToG1 is the non-panicking core of HashToG1. ok is false only if the
172// try-and-increment loop exhausts its bound, which does not happen for any
173// message with a well-formed hash (probability of exhaustion is ~2^-64).
174func hashToG1(msg []byte) (pt []byte, ok bool) {
175	pt, _, ok = hashToG1WithCounter(msg)
176	return pt, ok
177}
178
179// hashToG1WithCounter is hashToG1 plus the retry counter it stopped at,
180// exposed for tests that check the counter against testdata/vectors.json.
181func hashToG1WithCounter(msg []byte) (pt []byte, ctr int, ok bool) {
182	three := [4]uint64{3, 0, 0, 0}
183	for ctr := 0; ctr < 64; ctr++ {
184		buf := make([]byte, 0, len(DOMAIN)+len(msg)+1)
185		buf = append(buf, DOMAIN...)
186		buf = append(buf, msg...)
187		buf = append(buf, byte(ctr))
188		h := sha256.Sum256(buf)
189
190		x := modexp.ModExp(h[:], []byte{1}, pBytes) // reduce mod P
191		x3 := modexp.ModExp(x, []byte{3}, pBytes)    // x^3 mod P
192		rhs := fromLimbs(addModP(toLimbs(x3), three, pLimbs))
193
194		y := modexp.ModExp(rhs, sqrtExpBytes, pBytes) // candidate sqrt
195		y2 := modexp.ModExp(y, []byte{2}, pBytes)
196		if !bytesEqual(y2, rhs) {
197			continue // not a quadratic residue
198		}
199
200		yl := toLimbs(y)
201		if negy := sub(pLimbs, yl); cmp(negy, yl) < 0 {
202			y = fromLimbs(negy) // canonical branch: numerically smaller root
203		}
204		out := make([]byte, 0, 64)
205		out = append(out, x...)
206		out = append(out, y...)
207		return out, ctr, true
208	}
209	return nil, 64, false
210}
211
212func bytesEqual(a, b []byte) bool {
213	if len(a) != len(b) {
214		return false
215	}
216	for i := range a {
217		if a[i] != b[i] {
218			return false
219		}
220	}
221	return true
222}
223
224// HashToG1 maps msg to a G1 point via try-and-increment. Returns 64 bytes
225// (x|y). Panics if the loop exhausts its 64-attempt bound.
226func HashToG1(msg []byte) []byte {
227	pt, ok := hashToG1(msg)
228	if !ok {
229		panic("bls254: hash_to_g1 exhausted")
230	}
231	return pt
232}
233
234// NegateG1 returns (x, P-y) for a 64-byte G1 point. Returns nil on bad input.
235func NegateG1(pt []byte) []byte {
236	return negateG1(pt)
237}
238
239func negateG1(pt []byte) []byte {
240	if len(pt) != 64 {
241		return nil
242	}
243	yl := toLimbs(pt[32:64])
244	var zero [4]uint64
245	out := make([]byte, 64)
246	copy(out[:32], pt[:32])
247	if cmp(yl, zero) == 0 {
248		// y == 0 has no distinct negation; leave as-is.
249		copy(out[32:], pt[32:64])
250		return out
251	}
252	copy(out[32:], fromLimbs(sub(pLimbs, yl)))
253	return out
254}
255
256// Verify reports whether sig is the BLS signature over msg under pubkey.
257// pubkey is 128 bytes (G2, imaginary-first), sig is 64 bytes (G1). Verify is
258// total: wrong lengths, unreduced coordinates, off-curve points and an
259// invalid precompile result all yield false. It never panics.
260func Verify(pubkey, msg, sig []byte) bool {
261	if len(pubkey) != 128 || len(sig) != 64 {
262		return false
263	}
264	h, ok := hashToG1(msg)
265	if !ok {
266		return false
267	}
268	negH := negateG1(h)
269	if negH == nil {
270		return false
271	}
272
273	in := make([]byte, 0, 384)
274	in = append(in, sig...)
275	in = append(in, g2GenBytes...)
276	in = append(in, negH...)
277	in = append(in, pubkey...)
278	if len(in) != 384 {
279		return false
280	}
281
282	out := bn254.PairingCheck(in)
283	// PairingCheck returns an empty NON-NIL slice on an invalid point or an
284	// unreduced coordinate; checking length (not nilness) is load-bearing.
285	if len(out) != bn254.PairingOutputSize {
286		return false
287	}
288	return out[len(out)-1] == 1
289}
290
291// RoundMessage builds the canonical message for a beacon round:
292// chainID || "|" || decimal(round).
293func RoundMessage(chainID string, round int64) []byte {
294	return []byte(chainID + "|" + strconv.FormatInt(round, 10))
295}