code wiki / (root) / nx_u256_mul.nx

nx_u256_mul.nx source

↩ module page · 172 lines · 6484 B

1// nx_u256_mul.nx -- 256 x 256 -> 512-bit unsigned multiplication. 2// 3// Phase 0b §I.3 piece 1b of the ECDSA-P256 arc. Extends the 4// nx_u256 limb primitives (commit 6ff89dd8) with the schoolbook 5// 8 x 8 limb multiplication that produces a 16-limb (512-bit) 6// wide product. This is the foundation for both: 7// - p256_field_mul (composes wide_mul + NIST P-256 Solinas 8// reduction; queued for piece 1c) 9// - p256_scalar_mul mod n (composes wide_mul + Barrett or 10// trial-subtract reduction mod the group order; queued) 11// 12// Split rationale: wide multiplication is a primitive in its own 13// right. Splitting it from reduce-mod-p means scalar arithmetic 14// (which needs reduce-mod-n, NOT reduce-mod-p) gets to reuse the 15// same wide_mul. Per Cardinal 9 (single-responsibility), this 16// primitive does ONE thing -- big-int multiply -- and the two 17// reduction primitives compose on top. 18// 19// Algorithm: schoolbook with carry propagation. 20// 21// For each i in 0..8: 22// carry = 0 23// For each j in 0..8: 24// s = out[i+j] + a[i] * b[j] + carry 25// out[i+j] = low_32(s) 26// carry = high_32(s) 27// out[i+8] = carry 28// 29// Each (a[i] * b[j] + out[i+j] + carry) intermediate fits in 30// u64 (max = (2^32 - 1)^2 + 2 * (2^32 - 1) = 2^64 - 1), so the 31// expression is computed in a single i64 register without 32// 128-bit emulation. Sign-extension concerns are handled by 33// AND-mask after right-shift -- arithmetic shift of a negative- 34// looking i64 still produces the correct high 32 bits when 35// masked with 0xFFFFFFFF. 36// 37// Wide-output layout: out_16[0] = LSB of product, out_16[15] = MSB. 38// Same little-endian limb order as the 8-limb inputs. 39// 40// Aliasing: out_16 MUST NOT overlap a or b (the algorithm reads 41// a[i], b[j] for every (i, j) while it streams writes into 42// out_16). Caller responsibility (no defensive check). 43// 44// What this primitive does NOT do: 45// - Reduction mod any specific prime (queued: p256_field_mul, 46// p256_scalar_mul). 47// - Squaring fast-path (~25% speedup; queued as 48// u256_sq_wide once needed). 49// - Constant-time guarantee against timing side channels. 50// ECDSA verify operates on PUBLIC inputs (cert pubkey + sig + 51// msg hash), so variable-time multiplication is acceptable 52// here. A constant-time variant would be required for ECDSA 53// SIGNING (which we don't ship: substrate is verify-only). 54// 55// Per Cardinal 9 (single-responsibility) and Cardinal 22 56// (composition over configuration). 57// 58// license_tier: INDEPENDENT_REDERIVE 59// genealogy_id: international-research-sources/nist/fips_186_5 60// lineage_id: nishi_u256_mul_q10 61 62// nx_safety_envelope: 63// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 64// sil_target: SIL1 65// evidence: [bulk_applied_2026-05-19, u256-wide-mul-foundation] 66// verdict: NOT_YET_EVALUATED 67 68import "nx_syscalls.nx" 69import "nx_u256.nx" 70 71const NX_U256_WIDE_LIMBS: i64 = 16 72const NX_U256_WIDE_BYTES: i64 = 64 73 74// Allocate a 16-limb (512-bit) wide buffer, zeroed. 75// Served from the u256 scratch arena (see nx_u256.nx) -- no per-op 76// mmap; reclaimed by the enclosing routine's nx_scratch frame. 77func u256_wide_alloc() -> *i64 { 78 return (nx_scratch(NX_U256_WIDE_LIMBS * 8)) as *i64 79} 80 81// out_16 = a * b (256x256 -> 512). No reduction. 82// 83// Aliasing: out_16 MUST be a distinct buffer from a and b. 84// Returns 0. 85func u256_mul_wide(out_16: *i64, a: *i64, b: *i64) -> i64 { 86 var k: i64 = 0 87 while k < NX_U256_WIDE_LIMBS { 88 out_16[k] = 0 89 k = k + 1 90 } 91 var i: i64 = 0 92 while i < NX_U256_LIMBS { 93 let ai: i64 = a[i] & NX_U256_LIMB_MASK 94 var carry: i64 = 0 95 var j: i64 = 0 96 while j < NX_U256_LIMBS { 97 let bj: i64 = b[j] & NX_U256_LIMB_MASK 98 let prev: i64 = out_16[i + j] & NX_U256_LIMB_MASK 99 let s: i64 = prev + ai * bj + carry 100 out_16[i + j] = s & NX_U256_LIMB_MASK 101 carry = (s >> NX_U256_LIMB_BITS) & NX_U256_LIMB_MASK 102 j = j + 1 103 } 104 out_16[i + 8] = carry 105 i = i + 1 106 } 107 return 0 108} 109 110// Unsigned 64-bit less-than (NishiLang `<` is signed): flip both sign bits so 111// signed order matches unsigned order. Returns 1 if a <u b, else 0. The carry 112// primitive for software add-with-carry in the 4x64 multiply (native ADC = G3). 113func u64_lt(a: i64, b: i64) -> i64 { 114 let fa: i64 = a ^ (1 << 63) 115 let fb: i64 = b ^ (1 << 63) 116 if fa < fb { return 1 } 117 return 0 118} 119 120// FAST-COMPILER PATH (2026-06-06): the intrinsic-optimized 4x64 multiply used __umulhi64 / __adc_acc, 121// which the FAST known-good compiler cannot parse -- and @ifdef is NOT honored on nx_build_run's 122// direct-compile path (it bypasses the macro preprocessor), so a guard could not exclude it. Since the 123// crypto callers (p256_field_mul / p256_modn) already use the pure 8x32 u256_mul_wide, and that IS the 124// verified byte-exact oracle, this twin now DELEGATES to it. Result: the whole crypto stack builds with 125// the fast compiler in SECONDS (no intrinsics anywhere), results bit-identical. The intrinsic fast-path 126// can be restored later behind a real macro-preprocessor build target if crypto runtime ever dominates. 127func u256_mul_wide_4x64(out_16: *i64, a: *i64, b: *i64) -> i64 { 128 return u256_mul_wide(out_16, a, b) 129} 130 131// Compare two wide (16-limb) values. Returns -1, 0, +1. 132func u256_wide_cmp(a_16: *i64, b_16: *i64) -> i64 { 133 var i: i64 = NX_U256_WIDE_LIMBS - 1 134 while i >= 0 { 135 let av: i64 = a_16[i] & NX_U256_LIMB_MASK 136 let bv: i64 = b_16[i] & NX_U256_LIMB_MASK 137 if av < bv { return 0 - 1 } 138 if av > bv { return 1 } 139 i = i - 1 140 } 141 return 0 142} 143 144// Returns 1 if the upper 8 limbs are all zero (wide value fits in 145// 256 bits), else 0. 146func u256_wide_fits_in_256(a_16: *i64) -> i64 { 147 var i: i64 = 8 148 var acc: i64 = 0 149 while i < NX_U256_WIDE_LIMBS { 150 acc = acc | (a_16[i] & NX_U256_LIMB_MASK) 151 i = i + 1 152 } 153 if acc == 0 { return 1 } 154 return 0 155} 156 157// Copy the low 8 limbs of a 16-limb wide value into an 8-limb 158// narrow buffer. Used by reducers after they've folded the high 159// limbs into the low ones. 160func u256_wide_copy_low(out_8: *i64, src_16: *i64) -> i64 { 161 var i: i64 = 0 162 while i < NX_U256_LIMBS { 163 out_8[i] = src_16[i] 164 i = i + 1 165 } 166 return 0 167} 168 169// Compile-only smoke. Real KAT in nx_u256_mul_test.nx. 170func main() -> i64 { 171 return 0 172}