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.

fieldmul.gno

16.68 Kb · 378 lines
  1package bls254
  2
  3// Field multiplication mod P. TWO implementations, and this header records which
  4// one the drand path uses and the measurement that decided it. PHASE 2 SETTLED
  5// THIS; please do not re-litigate it from a gas table that came from somewhere
  6// else, because that is exactly how it went wrong the first time.
  7//
  8// The rest of this package gets away with crypto/modexp for every field
  9// operation it needs, because try-and-increment hash-to-G1 only ever needs
 10// powers (x^3, sqrt) and one addition. The RFC 9380 SVDW map that the drand
 11// path needs (see drandcurve.gno) is different: it needs about a dozen general
 12// products a*b per field element mapped, and modexp cannot multiply two
 13// distinct operands.
 14//
 15// Two ways out:
 16//
 17//  1. The identity a*b == ((a+b)^2 - (a-b)^2) / 4, which crypto/modexp can do
 18//     because it can square. Two native modexp calls, one modular subtraction and
 19//     a division by 4, which mod P is two halvings and not a third multiply.
 20//     Implemented below as mulModPIdentity / mulModPLimbs.
 21//  2. A hand-rolled 4-limb Montgomery multiply over math/bits.Mul64. No native
 22//     call, 32 64x64 multiplies per montMul. Implemented below as mulModP.
 23//
 24// (1) WINS, AND IT IS WHAT drandcurve.gno's fpMul AND fpSqr BIND TO.
 25//
 26// Measured on this toolchain (/home/johnny/go/bin/gno, gno v1.2.0 stdlibs) from
 27// gno test's own per-test GAS line, 100 iterations per test with the 349,766-gas
 28// empty-test baseline subtracted. The tests are in drandgas_test.gno; re-run them
 29// rather than trusting these numbers.
 30//
 31//	operation                                    gas each
 32//	mulModP          4-limb Montgomery          2,200,369
 33//	mulModPIdentity  identity, byte domain        519,791   <- 4.23x cheaper
 34//	mulModPLimbs     identity, limb domain        387,943
 35//	sqrModP          Montgomery a*a             2,200,184
 36//	sqrModPNative    one native modexp             69,602   <- 31.6x cheaper
 37//	modexp.ModExp(x, 2, P) bare                    23,145
 38//	modexp.ModExp(x, (P+1)/4, P) bare             134,372
 39//	toLimbs / fromLimbs                    39,328 / 42,308
 40//	halveModP                                      21,035
 41//
 42// End to end, net of the same baseline:
 43//
 44//	                        Montgomery      identity      factor
 45//	VerifyDrandOnce         63,846,571    13,396,451       4.77x
 46//	VerifyDrandFour / 4     68,401,101    15,106,773       4.53x
 47//	HashToG1DrandOnce       62,394,145    12,004,207       5.20x
 48//	MapToPointSVDWOnce      35,797,798     6,657,484       5.38x
 49//	r/splash/beacon Publish 74,891,351    16,671,000*      4.49x
 50//
 51// THE IDENTITY COLUMN ROSE ON 2026-09-29 AND THE REASON IS ONE LINE OF CODE, NOT DRIFT.
 52// mulModPLimbs gained the input guard AUDIT-2026-09-28.md finding 21 asked for -- two
 53// cmp calls against pLimbs, measured at 8,500 gas the pair -- so every multiply costs
 54// 8,500 more and the end-to-end figures rise by 8,500 times the number of multiplies
 55// they perform (16 in VerifyDrandOnce, 9 in the SVDW map). VerifyDrand is up 1.0 %.
 56// The previous figures were 511,291 / 379,443 / 13,260,451 / 14,964,398 / 11,868,207 /
 57// 6,580,984, and they are recorded here rather than silently replaced so that a reader
 58// who remembers them learns why they moved. mulModP, sqrModP and sqrModPNative are
 59// unchanged because the guard is not on their path.
 60//
 61// *THE Publish ROW IS DERIVED, NOT RE-ISOLATED, and it is the only number in this file
 62// that is. A Publish is its own fixed cost plus exactly one VerifyDrand, so it rises by
 63// the same 136,000; r/splash/beacon's own suite has no single-Publish gas test to read
 64// it off, and this change may not touch that realm. Re-isolate it there before quoting
 65// it as measured.
 66//
 67// WHY THE ORIGINAL CHOICE WENT THE OTHER WAY. (2) was specified on the strength of
 68// a gas table reading "modexp: Base 58000 + 24647680*len(mod)/1024", which would
 69// have made a modexp call cost ~800k. That table is from a different gno revision
 70// -- this host has two pinned toolchains, and it was read off GnoBuilder's
 71// c4c72fdd instead of SPLASH's v1.2.0. On v1.2.0 the crypto/modexp row is Base
 72// 1400, Slope 2200 on SizeModExpWork (a count of modular multiplications) and
 73// Slope2 3500 per 1024 bytes of modulus: a 254-bit squaring costs 23k, not 800k.
 74// Interpreted 64-bit limb arithmetic is the expensive thing here, not the native
 75// call. If you are about to change this back, measure on the toolchain you will
 76// deploy with first.
 77//
 78// MONTGOMERY IS KEPT, NOT DEAD CODE. mulModP is the independent oracle that
 79// TestMulModPIdentityMatchesMontgomery compares the identity multiply against:
 80// two implementations sharing no arithmetic, checked byte-for-byte over every
 81// ordered pair of 15 limb-boundary edge values, the python-derived vector table,
 82// 120 in-VM pseudorandom pairs and all 11 rejection cases, with the tally
 83// asserting that a+b wrapping, a-b going negative and both halvings' odd and even
 84// branches were each actually reached. Deleting mulModP would delete that oracle.
 85//
 86// WHAT IS STILL ON THE TABLE, measured rather than guessed. The multiply is now
 87// dominated by byte<->limb marshalling, not arithmetic: mulModPLimbs is 387,943
 88// against mulModPIdentity's 519,791, purely because the caller holds limbs. Moving
 89// mapToPointSVDW itself into the limb domain would save ~132k on each of its ~8
 90// products and ~120k on each of its ~6 additions, but would cost ~34k on each of
 91// its ~5 squarings (a squaring is cheapest in the byte domain, where it is one
 92// bare modexp) and ~42-81k on each of its 3 full exponentiations. Net estimate
 93// ~1.4M off a 6.66M map, so VerifyDrand ~13.40M -> ~10.5M: a further 21% for a
 94// rewrite of the most correctness-critical function in the package. Not taken.
 95//
 96// AND SUCH A REWRITE WOULD BE THE FIRST REAL LIMB-DOMAIN CALLER OF mulModPLimbs, which
 97// is precisely who finding 21's guard is for: the byte-domain wrappers would no longer
 98// be standing between the SVDW map and the unreduced-operand case.
 99// 0.0165 GNOT a relay is already 4.5x inside the budget that prompted this work.
100//
101// One cheap, safe win was left on the table deliberately: inlining mac into the
102// two loops below measured 360k instead of 503k gas for mul512, a 28% saving.
103// mul512 is now off the hot path entirely -- it is only reached through the
104// oracle -- so readability of the reduction loop wins outright and mac stays a
105// function.
106//
107// Everything here is unexported. mulModP, mulModPIdentity and sqrModPNative all
108// take and return 32-byte big-endian slices, matching modexp and the point
109// encodings used everywhere else in this package, and all three return nil for an
110// operand that is not exactly 32 bytes or is >= P.
111
112import (
113	"crypto/modexp"
114	"math/bits"
115)
116
117const (
118	// pInvNeg64 is -P^{-1} mod 2^64, the Montgomery reduction constant.
119	//
120	// Derived, not recalled:
121	//   python3 -c "p=0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47;
122	//               print(hex((-pow(p,-1,1<<256)) % (1<<64)))"
123	//   -> 0x87d20782e4866389
124	// TestMontgomeryConstants re-derives it inside the VM from pLimbs alone,
125	// so a typo here fails the test suite rather than silently producing
126	// wrong products.
127	pInvNeg64 = 0x87d20782e4866389
128
129	// r2Hex is R^2 mod P where R = 2^256, i.e. 2^512 mod P. montMul(x, r2)
130	// converts x into Montgomery form; montMul(x, 1) converts back.
131	//
132	//   python3 -c "p=0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47;
133	//               print('%064x' % pow(2,512,p))"
134	r2Hex = "06d89f71cab8351f47ab1eff0a417ff6b5e71911d44501fbf32cfc5b538afa89"
135)
136
137var r2Limbs = toLimbs(mustDecodeHex(r2Hex))
138
139// mac returns the 128-bit a*b + c + carryIn as (hi, lo).
140//
141// It cannot overflow: a*b <= (2^64-1)^2 = 2^128 - 2^65 + 1, so hi <= 2^64 - 2
142// and the two carries that can come out of the low word still leave hi below
143// 2^64.
144func mac(a, b, c, carryIn uint64) (hi, lo uint64) {
145	hi, lo = bits.Mul64(a, b)
146	var c1 uint64
147	lo, c1 = bits.Add64(lo, c, 0)
148	hi, _ = bits.Add64(hi, 0, c1)
149	lo, c1 = bits.Add64(lo, carryIn, 0)
150	hi, _ = bits.Add64(hi, 0, c1)
151	return hi, lo
152}
153
154// mul512 is the schoolbook 4x4 -> 8 limb product. Limbs are little-endian
155// (index 0 least significant), matching toLimbs.
156func mul512(a, b [4]uint64) [8]uint64 {
157	var t [8]uint64
158	for i := 0; i < 4; i++ {
159		var carry uint64
160		for j := 0; j < 4; j++ {
161			carry, t[i+j] = mac(a[j], b[i], t[i+j], carry)
162		}
163		// t[i+4] has not been written by any earlier i, so this is an
164		// assignment rather than an add.
165		t[i+4] = carry
166	}
167	return t
168}
169
170// montReduce returns t * R^{-1} mod P for t < P*R, by separated-operand-scanning
171// Montgomery reduction.
172func montReduce(t [8]uint64) [4]uint64 {
173	// hi9 is the notional ninth limb. The loop can carry past t[7] because
174	// t + m_i*P*2^(64i) can exceed 2^512 even though the final quotient does
175	// not; see the note on the return value below.
176	var hi9 uint64
177
178	for i := 0; i < 4; i++ {
179		m := t[i] * pInvNeg64
180		var carry uint64
181		for j := 0; j < 4; j++ {
182			carry, t[i+j] = mac(m, pLimbs[j], t[i+j], carry)
183		}
184		// t[i] is now zero by construction. Propagate the high carry.
185		for k := i + 4; k < 8 && carry != 0; k++ {
186			t[k], carry = bits.Add64(t[k], carry, 0)
187		}
188		hi9 += carry
189	}
190
191	r := [4]uint64{t[4], t[5], t[6], t[7]}
192
193	// For a, b < P the input to this function is a*b < P^2 < P*R, so the
194	// quotient is below P + P^2/R < 2P. P < 2^254, so 2P < 2^255 and hi9 is
195	// necessarily zero. Fold it into the reduction condition anyway: if a
196	// caller ever feeds an unreduced operand the answer stays in range for
197	// the single subtraction below instead of silently losing 2^256.
198	if hi9 != 0 || cmp(r, pLimbs) >= 0 {
199		r = sub(r, pLimbs)
200	}
201	return r
202}
203
204// montMul returns a*b*R^{-1} mod P for a, b reduced mod P.
205func montMul(a, b [4]uint64) [4]uint64 {
206	return montReduce(mul512(a, b))
207}
208
209// mulModP returns (a*b) mod P. a and b must be 32-byte big-endian values below
210// P; the result is 32 bytes big-endian. Returns nil if either input is not
211// exactly 32 bytes or is >= P -- an unreduced operand is a caller bug, and
212// returning a plausible-looking wrong product for it is the one outcome this
213// package cannot afford.
214func mulModP(a, b []byte) []byte {
215	if len(a) != 32 || len(b) != 32 {
216		return nil
217	}
218	al := toLimbs(a)
219	bl := toLimbs(b)
220	if cmp(al, pLimbs) >= 0 || cmp(bl, pLimbs) >= 0 {
221		return nil
222	}
223	// montMul(a, b) = a*b*R^{-1}; multiplying that by R^2 gives a*b.
224	return fromLimbs(montMul(montMul(al, bl), r2Limbs))
225}
226
227// sqrModP returns (a*a) mod P, with the same contract as mulModP.
228func sqrModP(a []byte) []byte { return mulModP(a, a) }
229
230// ---- Approach (1): the squaring identity, via native crypto/modexp ----
231//
232// This is the alternative the header calls the easier one. It is here because it
233// is also, measured on this toolchain, the much cheaper one -- see the table at
234// the bottom of this file.
235//
236//	a*b == ((a+b)^2 - (a-b)^2) / 4    (mod P)
237//
238// The identity is exact over the integers: (a+b)^2 - (a-b)^2 = 4ab. Reducing
239// a+b and a-b mod P first does not change their squares mod P, so the numerator
240// is congruent to 4ab and the division by 4 -- which mod P means multiplying by
241// the inverse of 4, i.e. halving twice -- recovers ab.
242
243// halveModP returns (x / 2) mod P, i.e. x * inverse(2) mod P, for x reduced mod P.
244//
245// P is odd, so inverse(2) exists. If x is even the integer x/2 is already the
246// answer. If x is odd then x+P is even and (x+P)/2 is the answer, because
247// 2*((x+P)/2) = x+P == x (mod P).
248//
249// No overflow: P < 2^254 and x < P, so x+P < 2^255. The carry out of add is
250// therefore always zero; it is folded into the shift anyway so that an unreduced
251// input cannot silently lose the top bit.
252func halveModP(x [4]uint64) [4]uint64 {
253	var carry uint64
254	if x[0]&1 == 1 {
255		x, carry = add(x, pLimbs)
256	}
257	// Shift right by one, pulling in carry as bit 256.
258	var r [4]uint64
259	r[0] = x[0]>>1 | x[1]<<63
260	r[1] = x[1]>>1 | x[2]<<63
261	r[2] = x[2]>>1 | x[3]<<63
262	r[3] = x[3]>>1 | carry<<63
263	return r
264}
265
266// sqrModPNative returns (a*a) mod P using one native modexp, with exactly
267// mulModP's input contract: nil unless a is 32 bytes and below P.
268//
269// modexp.ModExp returns len(modulus) == 32 bytes, left-padded, so the result
270// needs no adjustment.
271func sqrModPNative(a []byte) []byte {
272	if len(a) != 32 {
273		return nil
274	}
275	if cmp(toLimbs(a), pLimbs) >= 0 {
276		return nil
277	}
278	return modexp.ModExp(a, twoBytes, pBytes)
279}
280
281// twoBytes is the exponent 2. A one-byte exponent is what modexp's gas row
282// charges least for.
283var twoBytes = []byte{2}
284
285// mulModPIdentity returns (a*b) mod P via the squaring identity above. Its
286// contract is identical to mulModP's, including returning nil for a non-32-byte
287// or unreduced operand: TestMulModPIdentityMatchesMontgomery pins the two
288// functions to agreeing on every input, rejections included.
289func mulModPIdentity(a, b []byte) []byte {
290	if len(a) != 32 || len(b) != 32 {
291		return nil
292	}
293	al := toLimbs(a)
294	bl := toLimbs(b)
295	if cmp(al, pLimbs) >= 0 || cmp(bl, pLimbs) >= 0 {
296		return nil
297	}
298
299	return fromLimbs(mulModPLimbs(al, bl))
300}
301
302// mulModPLimbs is the identity multiply in the limb domain, for callers that
303// already hold reduced limbs and want reduced limbs back. Both operands MUST be
304// below P, AND SINCE 2026-09-29 THAT IS CHECKED HERE RATHER THAN ASSUMED.
305//
306// THE DEFECT THAT PUT THE CHECK IN, AUDIT-2026-09-28.md finding 21. This comment used
307// to read "there is no check, because the byte-domain wrappers do it and the SVDW map
308// only ever feeds it values it has already reduced". Both halves of that were true, and
309// the function was still THE ONLY PRODUCER IN THIS PACKAGE THAT WAS NOT FAIL-CLOSED.
310// Fed an unreduced b with b > a + P, subModP's `a < b` branch computes a + (P - b), and
311// P - b wraps mod 2^256 because b > P -- so the result is NOT congruent to a - b, the
312// numerator is not congruent to 4ab, and the function returns A PLAUSIBLE-LOOKING WRONG
313// PRODUCT. This package's own header calls that the one outcome it cannot afford: every
314// other entry point returns nil rather than a value that is merely probably right.
315// mulModP, mulModPIdentity and sqrModPNative all check; this did not.
316//
317// IT IS A PANIC AND NOT A nil BECAUSE THE RETURN TYPE HAS NO nil. [4]uint64 has no
318// invalid value to hand back, and its zero value is a legitimate product, so the only
319// fail-closed answer available is to stop. That is the choice hashToFieldDrand and
320// hash_to_g1 already make for their own impossible states.
321//
322// IT IS UNREACHABLE TODAY AND THE GUARD IS THEREFORE A CONTINGENCY, NOT A FIX TO A LIVE
323// BUG: the only non-test caller is mulModPIdentity, which performs exactly this
324// comparison on the same values first, and drandcurve.gno's fpMul and fpSqr still bind
325// to the input-checked mulModPIdentity and sqrModPNative. NOTHING ON THE LIVE PATH
326// CHANGED, and the measured cost of the redundant pair of cmp calls is in the header's
327// table. What changed is that a FUTURE caller in the limb domain -- which is the whole
328// reason this function is factored out at all -- cannot silently get a wrong answer.
329//
330// This exists because the byte<->limb conversions, not the arithmetic, are what
331// this multiply actually costs. toLimbs and fromLimbs measure ~53k and ~58k gas
332// each (see drandgas_test.gno); the byte-domain wrapper pays five conversions,
333// this core pays the four that the modexp boundary makes unavoidable.
334func mulModPLimbs(al, bl [4]uint64) [4]uint64 {
335	if cmp(al, pLimbs) >= 0 || cmp(bl, pLimbs) >= 0 {
336		panic("bls254: mulModPLimbs: operand is not reduced mod P")
337	}
338
339	// s = (a+b) mod P, d = (a-b) mod P. addModP handles the carry out of a+b
340	// exceeding P; subModP handles a < b by adding P back.
341	s := fromLimbs(addModP(al, bl, pLimbs))
342	d := fromLimbs(subModP(al, bl))
343
344	// Two native squarings. Both operands are already reduced.
345	s2 := modexp.ModExp(s, twoBytes, pBytes)
346	d2 := modexp.ModExp(d, twoBytes, pBytes)
347
348	// n == 4ab (mod P); halve twice to divide by 4.
349	n := subModP(toLimbs(s2), toLimbs(d2))
350	return halveModP(halveModP(n))
351}
352
353// subModP returns (a - b) mod P for a, b reduced mod P.
354//
355// THE PRECONDITION IS LOAD-BEARING AND IS WHERE FINDING 21 ACTUALLY LIVED. On the
356// `a < b` branch it computes a + (P - b), and sub() wraps mod 2^256: so for b > P the
357// term P - b is a huge wrong number and the result is not congruent to a - b. There is
358// no way to detect that from inside this function -- every [4]uint64 is a valid return
359// value -- so the contract is enforced by its callers, and mulModPLimbs states it as a
360// panic rather than assuming it.
361func subModP(a, b [4]uint64) [4]uint64 {
362	if cmp(a, b) >= 0 {
363		return sub(a, b)
364	}
365	// a < b: a - b + P, computed as a + (P - b). P - b is in [1, P] and a is
366	// below b <= P-1, so the sum is below P and no carry escapes.
367	d, _ := add(a, sub(pLimbs, b))
368	return d
369}
370
371// negModP returns (-a) mod P for a reduced mod P.
372func negModP(a [4]uint64) [4]uint64 {
373	var zero [4]uint64
374	if cmp(a, zero) == 0 {
375		return zero
376	}
377	return sub(pLimbs, a)
378}