Skip to main content

runar_compiler_rust/codegen/
ec.rs

1//! EC codegen — secp256k1 elliptic curve operations for Bitcoin Script.
2//!
3//! Port of packages/runar-compiler/src/passes/ec-codegen.ts.
4//! All helpers are self-contained.
5//!
6//! Point representation: 64 bytes (x[32] || y[32], big-endian unsigned).
7//! Internal arithmetic uses Jacobian coordinates for scalar multiplication.
8
9use super::stack::{PushValue, StackOp};
10
11// ===========================================================================
12// Constants
13// ===========================================================================
14
15/// Low 32 bits of (p - 2) = 0xFFFFFC2D.
16const FIELD_P_MINUS_2_LOW32: u32 = 0xFFFF_FC2D;
17
18/// 3 * secp256k1 curve order as a script number (little-endian sign-magnitude).
19/// Pre-computed to match TS constant-fold output (TS folds N+N+N → 3*N).
20const THREE_CURVE_N_SCRIPT_NUM: [u8; 33] = [
21    0xc3, 0xc3, 0xa2, 0x70, 0xa6, 0x1b, 0x77, 0x3f, 0xb3, 0xe0, 0xd9, 0x0d,
22    0xb4, 0x96, 0x0c, 0x30, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
23    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02,
24];
25
26/// secp256k1 generator x-coordinate (32 bytes, big-endian).
27const GEN_X_BYTES: [u8; 32] = [
28    0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95,
29    0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9,
30    0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
31];
32
33/// secp256k1 generator y-coordinate (32 bytes, big-endian).
34const GEN_Y_BYTES: [u8; 32] = [
35    0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc,
36    0x0e, 0x11, 0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19,
37    0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8,
38];
39
40/// Collect ops into a Vec via closure.
41fn collect_ops(f: impl FnOnce(&mut dyn FnMut(StackOp))) -> Vec<StackOp> {
42    let mut ops = Vec::new();
43    f(&mut |op| ops.push(op));
44    ops
45}
46
47// ===========================================================================
48// ECTracker — named stack state tracker (mirrors SLHTracker)
49// ===========================================================================
50
51struct ECTracker<'a> {
52    nm: Vec<String>,
53    e: &'a mut dyn FnMut(StackOp),
54}
55
56#[allow(dead_code)]
57impl<'a> ECTracker<'a> {
58    fn new(init: &[&str], emit: &'a mut dyn FnMut(StackOp)) -> Self {
59        ECTracker {
60            nm: init.iter().map(|s| s.to_string()).collect(),
61            e: emit,
62        }
63    }
64
65    fn depth(&self) -> usize {
66        self.nm.len()
67    }
68
69    fn find_depth(&self, name: &str) -> usize {
70        for i in (0..self.nm.len()).rev() {
71            if self.nm[i] == name {
72                return self.nm.len() - 1 - i;
73            }
74        }
75        panic!("ECTracker: '{}' not on stack {:?}", name, self.nm);
76    }
77
78    fn push_bytes(&mut self, n: &str, v: Vec<u8>) {
79        (self.e)(StackOp::Push(PushValue::Bytes(v)));
80        self.nm.push(n.to_string());
81    }
82
83    fn push_int(&mut self, n: &str, v: i128) {
84        (self.e)(StackOp::Push(PushValue::Int(v)));
85        self.nm.push(n.to_string());
86    }
87
88    fn dup(&mut self, n: &str) {
89        (self.e)(StackOp::Dup);
90        self.nm.push(n.to_string());
91    }
92
93    fn drop(&mut self) {
94        (self.e)(StackOp::Drop);
95        if !self.nm.is_empty() {
96            self.nm.pop();
97        }
98    }
99
100    fn nip(&mut self) {
101        (self.e)(StackOp::Nip);
102        let len = self.nm.len();
103        if len >= 2 {
104            self.nm.remove(len - 2);
105        }
106    }
107
108    fn over(&mut self, n: &str) {
109        (self.e)(StackOp::Over);
110        self.nm.push(n.to_string());
111    }
112
113    fn swap(&mut self) {
114        (self.e)(StackOp::Swap);
115        let len = self.nm.len();
116        if len >= 2 {
117            self.nm.swap(len - 1, len - 2);
118        }
119    }
120
121    fn rot(&mut self) {
122        (self.e)(StackOp::Rot);
123        let len = self.nm.len();
124        if len >= 3 {
125            let r = self.nm.remove(len - 3);
126            self.nm.push(r);
127        }
128    }
129
130    fn op(&mut self, code: &str) {
131        (self.e)(StackOp::Opcode(code.into()));
132    }
133
134    fn roll(&mut self, d: usize) {
135        if d == 0 {
136            return;
137        }
138        if d == 1 {
139            self.swap();
140            return;
141        }
142        if d == 2 {
143            self.rot();
144            return;
145        }
146        (self.e)(StackOp::Push(PushValue::Int(d as i128)));
147        self.nm.push(String::new());
148        (self.e)(StackOp::Opcode("OP_ROLL".into()));
149        self.nm.pop(); // pop the push
150        let idx = self.nm.len() - 1 - d;
151        let r = self.nm.remove(idx);
152        self.nm.push(r);
153    }
154
155    fn pick(&mut self, d: usize, n: &str) {
156        if d == 0 {
157            self.dup(n);
158            return;
159        }
160        if d == 1 {
161            self.over(n);
162            return;
163        }
164        (self.e)(StackOp::Push(PushValue::Int(d as i128)));
165        self.nm.push(String::new());
166        (self.e)(StackOp::Opcode("OP_PICK".into()));
167        self.nm.pop(); // pop the push
168        self.nm.push(n.to_string());
169    }
170
171    fn to_top(&mut self, name: &str) {
172        let d = self.find_depth(name);
173        self.roll(d);
174    }
175
176    fn copy_to_top(&mut self, name: &str, n: &str) {
177        let d = self.find_depth(name);
178        self.pick(d, n);
179    }
180
181    fn to_alt(&mut self) {
182        self.op("OP_TOALTSTACK");
183        if !self.nm.is_empty() {
184            self.nm.pop();
185        }
186    }
187
188    fn from_alt(&mut self, n: &str) {
189        self.op("OP_FROMALTSTACK");
190        self.nm.push(n.to_string());
191    }
192
193    fn rename(&mut self, n: &str) {
194        if let Some(last) = self.nm.last_mut() {
195            *last = n.to_string();
196        }
197    }
198
199    /// Emit raw opcodes; tracker only records net stack effect.
200    fn raw_block(
201        &mut self,
202        consume: &[&str],
203        produce: Option<&str>,
204        f: impl FnOnce(&mut dyn FnMut(StackOp)),
205    ) {
206        for _ in consume {
207            if !self.nm.is_empty() {
208                self.nm.pop();
209            }
210        }
211        f(self.e);
212        if let Some(p) = produce {
213            self.nm.push(p.to_string());
214        }
215    }
216
217    /// Emit if/else with tracked stack effect.
218    fn emit_if(
219        &mut self,
220        cond_name: &str,
221        then_fn: impl FnOnce(&mut dyn FnMut(StackOp)),
222        else_fn: impl FnOnce(&mut dyn FnMut(StackOp)),
223        result_name: Option<&str>,
224    ) {
225        self.to_top(cond_name);
226        self.nm.pop(); // condition consumed
227        let then_ops = collect_ops(then_fn);
228        let else_ops = collect_ops(else_fn);
229        (self.e)(StackOp::If {
230            then_ops,
231            else_ops,
232        });
233        if let Some(rn) = result_name {
234            self.nm.push(rn.to_string());
235        }
236    }
237}
238
239// ===========================================================================
240// Field arithmetic helpers
241// ===========================================================================
242
243/// secp256k1 field prime p as a Bitcoin script number (little-endian sign-magnitude).
244/// p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
245/// Big-endian bytes [0..31]:
246///   [ff]*27, fe, ff, ff, fc, 2f
247/// Reversed to LE (byte 31 first):
248///   2f, fc, ff, ff, fe, [ff]*27
249/// MSB (0xff) has bit 7 set, so we append a 0x00 sign byte to keep it positive.
250const FIELD_P_SCRIPT_NUM: [u8; 33] = [
251    0x2f, 0xfc, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
252    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
253    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00,
254];
255
256/// Push the field prime p onto the stack as a script number.
257fn push_field_p(t: &mut ECTracker, name: &str) {
258    // Push p as pre-encoded script number bytes — equivalent to pushInt(FIELD_P)
259    // in the TS implementation, but using bytes since FIELD_P exceeds i128.
260    t.push_bytes(name, FIELD_P_SCRIPT_NUM.to_vec());
261}
262
263/// fieldMod: reduce TOS mod p, ensure non-negative.
264/// Expects `a_name` to be on the tracker stack.
265fn field_mod(t: &mut ECTracker, a_name: &str, result_name: &str) {
266    t.to_top(a_name);
267    push_field_p(t, "_fmod_p");
268    // (a % p + p) % p
269    t.raw_block(&[a_name, "_fmod_p"], Some(result_name), |e| {
270        e(StackOp::Opcode("OP_2DUP".into())); // a p a p
271        e(StackOp::Opcode("OP_MOD".into()));   // a p (a%p)
272        e(StackOp::Rot);                        // p (a%p) a
273        e(StackOp::Drop);                       // p (a%p)
274        e(StackOp::Over);                       // p (a%p) p
275        e(StackOp::Opcode("OP_ADD".into()));    // p (a%p+p)
276        e(StackOp::Swap);                       // (a%p+p) p
277        e(StackOp::Opcode("OP_MOD".into()));    // ((a%p+p)%p)
278    });
279}
280
281/// fieldAdd: (a + b) mod p.
282fn field_add(t: &mut ECTracker, a_name: &str, b_name: &str, result_name: &str) {
283    t.to_top(a_name);
284    t.to_top(b_name);
285    t.raw_block(&[a_name, b_name], Some("_fadd_sum"), |e| {
286        e(StackOp::Opcode("OP_ADD".into()));
287    });
288    field_mod(t, "_fadd_sum", result_name);
289}
290
291/// fieldSub: (a - b) mod p (non-negative).
292fn field_sub(t: &mut ECTracker, a_name: &str, b_name: &str, result_name: &str) {
293    t.to_top(a_name);
294    t.to_top(b_name);
295    t.raw_block(&[a_name, b_name], Some("_fsub_diff"), |e| {
296        e(StackOp::Opcode("OP_SUB".into()));
297    });
298    field_mod(t, "_fsub_diff", result_name);
299}
300
301/// fieldMul: (a * b) mod p.
302fn field_mul(t: &mut ECTracker, a_name: &str, b_name: &str, result_name: &str) {
303    t.to_top(a_name);
304    t.to_top(b_name);
305    t.raw_block(&[a_name, b_name], Some("_fmul_prod"), |e| {
306        e(StackOp::Opcode("OP_MUL".into()));
307    });
308    field_mod(t, "_fmul_prod", result_name);
309}
310
311/// fieldSqr: (a * a) mod p.
312fn field_sqr(t: &mut ECTracker, a_name: &str, result_name: &str) {
313    t.copy_to_top(a_name, "_fsqr_copy");
314    field_mul(t, a_name, "_fsqr_copy", result_name);
315}
316
317/// fieldInv: a^(p-2) mod p via square-and-multiply.
318/// Consumes a_name from the tracker.
319fn field_inv(t: &mut ECTracker, a_name: &str, result_name: &str) {
320    // p-2 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2D
321    // Bits 255..32: 224 bits, all 1 except bit 32 which is 0
322    // Bits 31..0: 0xFFFFFC2D
323
324    // Start: result = a (bit 255 = 1)
325    t.copy_to_top(a_name, "_inv_r");
326    // Bits 254 down to 33: all 1's (222 bits). Bit 32 is 0 (handled below).
327    for _i in 0..222 {
328        field_sqr(t, "_inv_r", "_inv_r2");
329        t.rename("_inv_r");
330        t.copy_to_top(a_name, "_inv_a");
331        field_mul(t, "_inv_r", "_inv_a", "_inv_m");
332        t.rename("_inv_r");
333    }
334    // Bit 32 is 0: square only (no multiply)
335    field_sqr(t, "_inv_r", "_inv_r2");
336    t.rename("_inv_r");
337    // Bits 31 down to 0 of p-2
338    let low_bits = FIELD_P_MINUS_2_LOW32;
339    for i in (0..=31).rev() {
340        field_sqr(t, "_inv_r", "_inv_r2");
341        t.rename("_inv_r");
342        if (low_bits >> i) & 1 != 0 {
343            t.copy_to_top(a_name, "_inv_a");
344            field_mul(t, "_inv_r", "_inv_a", "_inv_m");
345            t.rename("_inv_r");
346        }
347    }
348    // Clean up original input and rename result
349    t.to_top(a_name);
350    t.drop();
351    t.to_top("_inv_r");
352    t.rename(result_name);
353}
354
355// ===========================================================================
356// Point decompose / compose
357// ===========================================================================
358
359/// Decompose 64-byte Point -> (x_num, y_num) on stack.
360/// Consumes pointName, produces xName and yName.
361fn decompose_point(t: &mut ECTracker, point_name: &str, x_name: &str, y_name: &str) {
362    t.to_top(point_name);
363    // OP_SPLIT at 32 produces x_bytes (bottom) and y_bytes (top)
364    t.raw_block(&[point_name], None, |e| {
365        e(StackOp::Push(PushValue::Int(32)));
366        e(StackOp::Opcode("OP_SPLIT".into()));
367    });
368    // Manually track the two new items
369    t.nm.push("_dp_xb".to_string());
370    t.nm.push("_dp_yb".to_string());
371
372    // Convert y_bytes (on top) to num
373    // Reverse from BE to LE, append 0x00 sign byte to ensure unsigned, then BIN2NUM
374    t.raw_block(&["_dp_yb"], Some(y_name), |e| {
375        emit_reverse_32(e);
376        e(StackOp::Push(PushValue::Bytes(vec![0x00])));
377        e(StackOp::Opcode("OP_CAT".into()));
378        e(StackOp::Opcode("OP_BIN2NUM".into()));
379    });
380
381    // Convert x_bytes to num
382    t.to_top("_dp_xb");
383    t.raw_block(&["_dp_xb"], Some(x_name), |e| {
384        emit_reverse_32(e);
385        e(StackOp::Push(PushValue::Bytes(vec![0x00])));
386        e(StackOp::Opcode("OP_CAT".into()));
387        e(StackOp::Opcode("OP_BIN2NUM".into()));
388    });
389
390    // Stack: [yName, xName] — swap to standard order [xName, yName]
391    t.swap();
392}
393
394/// Compose (x_num, y_num) -> 64-byte Point.
395/// Consumes xName and yName, produces resultName.
396fn compose_point(t: &mut ECTracker, x_name: &str, y_name: &str, result_name: &str) {
397    // Convert x to 32-byte big-endian
398    // Use NUM2BIN(33) to accommodate the sign byte, then drop the last byte
399    t.to_top(x_name);
400    t.raw_block(&[x_name], Some("_cp_xb"), |e| {
401        e(StackOp::Push(PushValue::Int(33)));
402        e(StackOp::Opcode("OP_NUM2BIN".into()));
403        // Drop the sign byte (last byte) — split at 32, keep left
404        e(StackOp::Push(PushValue::Int(32)));
405        e(StackOp::Opcode("OP_SPLIT".into()));
406        e(StackOp::Drop);
407        emit_reverse_32(e);
408    });
409
410    // Convert y to 32-byte big-endian
411    t.to_top(y_name);
412    t.raw_block(&[y_name], Some("_cp_yb"), |e| {
413        e(StackOp::Push(PushValue::Int(33)));
414        e(StackOp::Opcode("OP_NUM2BIN".into()));
415        e(StackOp::Push(PushValue::Int(32)));
416        e(StackOp::Opcode("OP_SPLIT".into()));
417        e(StackOp::Drop);
418        emit_reverse_32(e);
419    });
420
421    // Cat: x_be || y_be (x is below y after the two to_top calls)
422    t.to_top("_cp_xb");
423    t.to_top("_cp_yb");
424    t.raw_block(&["_cp_xb", "_cp_yb"], Some(result_name), |e| {
425        e(StackOp::Opcode("OP_CAT".into()));
426    });
427}
428
429/// Emit inline byte reversal for a 32-byte value on TOS.
430/// After: reversed 32-byte value on TOS.
431fn emit_reverse_32(e: &mut dyn FnMut(StackOp)) {
432    // Push empty accumulator, swap with data
433    e(StackOp::Opcode("OP_0".into()));
434    e(StackOp::Swap);
435    // 32 iterations: peel first byte, prepend to accumulator
436    for _i in 0..32 {
437        // Stack: [accum, remaining]
438        e(StackOp::Push(PushValue::Int(1)));
439        e(StackOp::Opcode("OP_SPLIT".into()));
440        // Stack: [accum, byte0, rest]
441        e(StackOp::Rot);
442        // Stack: [byte0, rest, accum]
443        e(StackOp::Rot);
444        // Stack: [rest, accum, byte0]
445        e(StackOp::Swap);
446        // Stack: [rest, byte0, accum]
447        e(StackOp::Opcode("OP_CAT".into()));
448        // Stack: [rest, byte0||accum]
449        e(StackOp::Swap);
450        // Stack: [byte0||accum, rest]
451    }
452    // Stack: [reversed, empty]
453    e(StackOp::Drop);
454}
455
456// ===========================================================================
457// Affine point addition (for ecAdd)
458// ===========================================================================
459
460/// Affine point addition: expects px, py, qx, qy on tracker.
461/// Produces rx, ry. Consumes all four inputs.
462fn affine_add(t: &mut ECTracker) {
463    // s_num = qy - py
464    t.copy_to_top("qy", "_qy1");
465    t.copy_to_top("py", "_py1");
466    field_sub(t, "_qy1", "_py1", "_s_num");
467
468    // s_den = qx - px
469    t.copy_to_top("qx", "_qx1");
470    t.copy_to_top("px", "_px1");
471    field_sub(t, "_qx1", "_px1", "_s_den");
472
473    // s = s_num / s_den mod p
474    field_inv(t, "_s_den", "_s_den_inv");
475    field_mul(t, "_s_num", "_s_den_inv", "_s");
476
477    // rx = s^2 - px - qx mod p
478    t.copy_to_top("_s", "_s_keep");
479    field_sqr(t, "_s", "_s2");
480    t.copy_to_top("px", "_px2");
481    field_sub(t, "_s2", "_px2", "_rx1");
482    t.copy_to_top("qx", "_qx2");
483    field_sub(t, "_rx1", "_qx2", "rx");
484
485    // ry = s * (px - rx) - py mod p
486    t.copy_to_top("px", "_px3");
487    t.copy_to_top("rx", "_rx2");
488    field_sub(t, "_px3", "_rx2", "_px_rx");
489    field_mul(t, "_s_keep", "_px_rx", "_s_px_rx");
490    t.copy_to_top("py", "_py2");
491    field_sub(t, "_s_px_rx", "_py2", "ry");
492
493    // Clean up original points
494    t.to_top("px"); t.drop();
495    t.to_top("py"); t.drop();
496    t.to_top("qx"); t.drop();
497    t.to_top("qy"); t.drop();
498}
499
500// ===========================================================================
501// Jacobian point operations (for ecMul)
502// ===========================================================================
503
504/// Jacobian point doubling (a=0 for secp256k1).
505/// Expects jx, jy, jz on tracker. Replaces with updated values.
506fn jacobian_double(t: &mut ECTracker) {
507    // Save copies of jx, jy, jz for later use
508    t.copy_to_top("jy", "_jy_save");
509    t.copy_to_top("jx", "_jx_save");
510    t.copy_to_top("jz", "_jz_save");
511
512    // A = jy^2
513    field_sqr(t, "jy", "_A");
514
515    // B = 4 * jx * A
516    t.copy_to_top("_A", "_A_save");
517    field_mul(t, "jx", "_A", "_xA");
518    t.push_int("_four", 4);
519    field_mul(t, "_xA", "_four", "_B");
520
521    // C = 8 * A^2
522    field_sqr(t, "_A_save", "_A2");
523    t.push_int("_eight", 8);
524    field_mul(t, "_A2", "_eight", "_C");
525
526    // D = 3 * X^2
527    field_sqr(t, "_jx_save", "_x2");
528    t.push_int("_three", 3);
529    field_mul(t, "_x2", "_three", "_D");
530
531    // nx = D^2 - 2*B
532    t.copy_to_top("_D", "_D_save");
533    t.copy_to_top("_B", "_B_save");
534    field_sqr(t, "_D", "_D2");
535    t.copy_to_top("_B", "_B1");
536    t.push_int("_two1", 2);
537    field_mul(t, "_B1", "_two1", "_2B");
538    field_sub(t, "_D2", "_2B", "_nx");
539
540    // ny = D*(B - nx) - C
541    t.copy_to_top("_nx", "_nx_copy");
542    field_sub(t, "_B_save", "_nx_copy", "_B_nx");
543    field_mul(t, "_D_save", "_B_nx", "_D_B_nx");
544    field_sub(t, "_D_B_nx", "_C", "_ny");
545
546    // nz = 2 * Y * Z
547    field_mul(t, "_jy_save", "_jz_save", "_yz");
548    t.push_int("_two2", 2);
549    field_mul(t, "_yz", "_two2", "_nz");
550
551    // Clean up leftovers: _B (used via _B_save/_B1) and old jz (only copied, never consumed)
552    t.to_top("_B"); t.drop();
553    t.to_top("jz"); t.drop();
554    t.to_top("_nx"); t.rename("jx");
555    t.to_top("_ny"); t.rename("jy");
556    t.to_top("_nz"); t.rename("jz");
557}
558
559/// Jacobian -> Affine conversion.
560/// Consumes jx, jy, jz; produces rx_name, ry_name.
561fn jacobian_to_affine(t: &mut ECTracker, rx_name: &str, ry_name: &str) {
562    field_inv(t, "jz", "_zinv");
563    t.copy_to_top("_zinv", "_zinv_keep");
564    field_sqr(t, "_zinv", "_zinv2");
565    t.copy_to_top("_zinv2", "_zinv2_keep");
566    field_mul(t, "_zinv_keep", "_zinv2", "_zinv3");
567    field_mul(t, "jx", "_zinv2_keep", rx_name);
568    field_mul(t, "jy", "_zinv3", ry_name);
569}
570
571// ===========================================================================
572// Jacobian mixed addition (P_jacobian + Q_affine)
573// ===========================================================================
574
575/// Build Jacobian mixed-add ops for use inside OP_IF.
576/// Uses an inner ECTracker to leverage field arithmetic helpers.
577///
578/// Stack layout: [..., ax, ay, _k, jx, jy, jz]
579/// After:        [..., ax, ay, _k, jx', jy', jz']
580fn build_jacobian_add_affine_inline(e: &mut dyn FnMut(StackOp), t: &ECTracker) {
581    // Create inner tracker with cloned stack state
582    let cloned_nm: Vec<String> = t.nm.clone();
583    let init_strs: Vec<&str> = cloned_nm.iter().map(|s| s.as_str()).collect();
584    let mut it = ECTracker::new(&init_strs, e);
585
586    // Save copies of values that get consumed but are needed later
587    it.copy_to_top("jz", "_jz_for_z1cu");   // consumed by Z1sq, needed for Z1cu
588    it.copy_to_top("jz", "_jz_for_z3");     // needed for Z3
589    it.copy_to_top("jy", "_jy_for_y3");     // consumed by R, needed for Y3
590    it.copy_to_top("jx", "_jx_for_u1h2");   // consumed by H, needed for U1H2
591
592    // Z1sq = jz^2
593    field_sqr(&mut it, "jz", "_Z1sq");
594
595    // Z1cu = _jz_for_z1cu * Z1sq (copy Z1sq for U2)
596    it.copy_to_top("_Z1sq", "_Z1sq_for_u2");
597    field_mul(&mut it, "_jz_for_z1cu", "_Z1sq", "_Z1cu");
598
599    // U2 = ax * Z1sq_for_u2
600    it.copy_to_top("ax", "_ax_c");
601    field_mul(&mut it, "_ax_c", "_Z1sq_for_u2", "_U2");
602
603    // S2 = ay * Z1cu
604    it.copy_to_top("ay", "_ay_c");
605    field_mul(&mut it, "_ay_c", "_Z1cu", "_S2");
606
607    // H = U2 - jx
608    field_sub(&mut it, "_U2", "jx", "_H");
609
610    // R = S2 - jy
611    field_sub(&mut it, "_S2", "jy", "_R");
612
613    // Save copies of H (consumed by H2 sqr, needed for H3 and Z3)
614    it.copy_to_top("_H", "_H_for_h3");
615    it.copy_to_top("_H", "_H_for_z3");
616
617    // H2 = H^2
618    field_sqr(&mut it, "_H", "_H2");
619
620    // Save H2 for U1H2
621    it.copy_to_top("_H2", "_H2_for_u1h2");
622
623    // H3 = H_for_h3 * H2
624    field_mul(&mut it, "_H_for_h3", "_H2", "_H3");
625
626    // U1H2 = _jx_for_u1h2 * H2_for_u1h2
627    field_mul(&mut it, "_jx_for_u1h2", "_H2_for_u1h2", "_U1H2");
628
629    // Save R, U1H2, H3 for Y3 computation
630    it.copy_to_top("_R", "_R_for_y3");
631    it.copy_to_top("_U1H2", "_U1H2_for_y3");
632    it.copy_to_top("_H3", "_H3_for_y3");
633
634    // X3 = R^2 - H3 - 2*U1H2
635    field_sqr(&mut it, "_R", "_R2");
636    field_sub(&mut it, "_R2", "_H3", "_x3_tmp");
637    it.push_int("_two", 2);
638    field_mul(&mut it, "_U1H2", "_two", "_2U1H2");
639    field_sub(&mut it, "_x3_tmp", "_2U1H2", "_X3");
640
641    // Y3 = R_for_y3*(U1H2_for_y3 - X3) - jy_for_y3*H3_for_y3
642    it.copy_to_top("_X3", "_X3_c");
643    field_sub(&mut it, "_U1H2_for_y3", "_X3_c", "_u_minus_x");
644    field_mul(&mut it, "_R_for_y3", "_u_minus_x", "_r_tmp");
645    field_mul(&mut it, "_jy_for_y3", "_H3_for_y3", "_jy_h3");
646    field_sub(&mut it, "_r_tmp", "_jy_h3", "_Y3");
647
648    // Z3 = _jz_for_z3 * _H_for_z3
649    field_mul(&mut it, "_jz_for_z3", "_H_for_z3", "_Z3");
650
651    // Rename results to jx/jy/jz
652    it.to_top("_X3"); it.rename("jx");
653    it.to_top("_Y3"); it.rename("jy");
654    it.to_top("_Z3"); it.rename("jz");
655}
656
657// ===========================================================================
658// Public entry points (called from stack lowerer)
659// ===========================================================================
660
661/// ecAdd: add two points.
662/// Stack in: [point_a, point_b] (b on top)
663/// Stack out: [result_point]
664pub fn emit_ec_add(emit: &mut dyn FnMut(StackOp)) {
665    let mut t = ECTracker::new(&["_pa", "_pb"], emit);
666    decompose_point(&mut t, "_pa", "px", "py");
667    decompose_point(&mut t, "_pb", "qx", "qy");
668    affine_add(&mut t);
669    compose_point(&mut t, "rx", "ry", "_result");
670}
671
672/// ecMul: scalar multiplication P * k.
673/// Stack in: [point, scalar] (scalar on top)
674/// Stack out: [result_point]
675///
676/// Uses 256-iteration double-and-add with Jacobian coordinates.
677pub fn emit_ec_mul(emit: &mut dyn FnMut(StackOp)) {
678    let mut t = ECTracker::new(&["_pt", "_k"], emit);
679    // Decompose to affine base point
680    decompose_point(&mut t, "_pt", "ax", "ay");
681
682    // k' = k + 3n: guarantees bit 257 is set.
683    // k ∈ [1, n-1], so k+3n ∈ [3n+1, 4n-1]. Since 3n > 2^257, bit 257
684    // is always 1. Adding 3n (≡ 0 mod n) preserves the EC point: k*G = (k+3n)*G.
685    // Push 3*N directly (matches TS constant-fold output).
686    t.to_top("_k");
687    t.push_bytes("_3n", THREE_CURVE_N_SCRIPT_NUM.to_vec());
688    t.raw_block(&["_k", "_3n"], Some("_k3n"), |e| {
689        e(StackOp::Opcode("OP_ADD".into()));
690    });
691    t.rename("_k");
692
693    // Init accumulator = P (bit 257 of k+3n is always 1)
694    t.copy_to_top("ax", "jx");
695    t.copy_to_top("ay", "jy");
696    t.push_int("jz", 1);
697
698    // 257 iterations: bits 256 down to 0
699    for bit in (0..=256).rev() {
700        // Double accumulator
701        jacobian_double(&mut t);
702
703        // Extract bit: (k >> bit) & 1, using OP_DIV for right-shift
704        t.copy_to_top("_k", "_k_copy");
705        if bit > 0 {
706            // divisor = 1 << bit — use Int for small values (matches TS OP_1..OP_16),
707            // script-number-encoded bytes for larger values that exceed i128.
708            if bit <= 126 {
709                t.push_int("_div", 1i128 << bit);
710            } else {
711                let divisor_bytes = script_number_pow2(bit);
712                t.push_bytes("_div", divisor_bytes);
713            }
714            t.raw_block(&["_k_copy", "_div"], Some("_shifted"), |e| {
715                e(StackOp::Opcode("OP_DIV".into()));
716            });
717        } else {
718            t.rename("_shifted");
719        }
720        t.push_int("_two", 2);
721        t.raw_block(&["_shifted", "_two"], Some("_bit"), |e| {
722            e(StackOp::Opcode("OP_MOD".into()));
723        });
724
725        // Move _bit to TOS and remove from tracker BEFORE generating add ops,
726        // because OP_IF consumes _bit and the add ops run with _bit already gone.
727        t.to_top("_bit");
728        t.nm.pop(); // _bit consumed by IF
729        let add_ops = collect_ops(|add_emit| {
730            build_jacobian_add_affine_inline(add_emit, &t);
731        });
732        (t.e)(StackOp::If {
733            then_ops: add_ops,
734            else_ops: vec![],
735        });
736    }
737
738    // Convert Jacobian to affine
739    jacobian_to_affine(&mut t, "_rx", "_ry");
740
741    // Clean up base point and scalar
742    t.to_top("ax"); t.drop();
743    t.to_top("ay"); t.drop();
744    t.to_top("_k"); t.drop();
745
746    // Compose result
747    compose_point(&mut t, "_rx", "_ry", "_result");
748}
749
750/// ecMulGen: scalar multiplication G * k.
751/// Stack in: [scalar]
752/// Stack out: [result_point]
753pub fn emit_ec_mul_gen(emit: &mut dyn FnMut(StackOp)) {
754    // Push generator point as 64-byte blob, then delegate to ecMul
755    let mut g_point = Vec::with_capacity(64);
756    g_point.extend_from_slice(&GEN_X_BYTES);
757    g_point.extend_from_slice(&GEN_Y_BYTES);
758    emit(StackOp::Push(PushValue::Bytes(g_point)));
759    emit(StackOp::Swap); // [point, scalar]
760    emit_ec_mul(emit);
761}
762
763/// ecNegate: negate a point (x, p - y).
764/// Stack in: [point]
765/// Stack out: [negated_point]
766pub fn emit_ec_negate(emit: &mut dyn FnMut(StackOp)) {
767    let mut t = ECTracker::new(&["_pt"], emit);
768    decompose_point(&mut t, "_pt", "_nx", "_ny");
769    push_field_p(&mut t, "_fp");
770    field_sub(&mut t, "_fp", "_ny", "_neg_y");
771    compose_point(&mut t, "_nx", "_neg_y", "_result");
772}
773
774/// ecOnCurve: check if point is on secp256k1 (y^2 = x^3 + 7 mod p).
775/// Stack in: [point]
776/// Stack out: [boolean]
777pub fn emit_ec_on_curve(emit: &mut dyn FnMut(StackOp)) {
778    let mut t = ECTracker::new(&["_pt"], emit);
779    decompose_point(&mut t, "_pt", "_x", "_y");
780
781    // lhs = y^2
782    field_sqr(&mut t, "_y", "_y2");
783
784    // rhs = x^3 + 7
785    t.copy_to_top("_x", "_x_copy");
786    field_sqr(&mut t, "_x", "_x2");
787    field_mul(&mut t, "_x2", "_x_copy", "_x3");
788    t.push_int("_seven", 7);
789    field_add(&mut t, "_x3", "_seven", "_rhs");
790
791    // Compare
792    t.to_top("_y2");
793    t.to_top("_rhs");
794    t.raw_block(&["_y2", "_rhs"], Some("_result"), |e| {
795        e(StackOp::Opcode("OP_EQUAL".into()));
796    });
797}
798
799/// ecModReduce: ((value % mod) + mod) % mod
800/// Stack in: [value, mod]
801/// Stack out: [result]
802pub fn emit_ec_mod_reduce(emit: &mut dyn FnMut(StackOp)) {
803    emit(StackOp::Opcode("OP_2DUP".into()));
804    emit(StackOp::Opcode("OP_MOD".into()));
805    emit(StackOp::Rot);
806    emit(StackOp::Drop);
807    emit(StackOp::Over);
808    emit(StackOp::Opcode("OP_ADD".into()));
809    emit(StackOp::Swap);
810    emit(StackOp::Opcode("OP_MOD".into()));
811}
812
813/// ecEncodeCompressed: point -> 33-byte compressed pubkey.
814/// Stack in: [point (64 bytes)]
815/// Stack out: [compressed (33 bytes)]
816pub fn emit_ec_encode_compressed(emit: &mut dyn FnMut(StackOp)) {
817    // Split at 32: [x_bytes, y_bytes]
818    emit(StackOp::Push(PushValue::Int(32)));
819    emit(StackOp::Opcode("OP_SPLIT".into()));
820    // Get last byte of y for parity
821    emit(StackOp::Opcode("OP_SIZE".into()));
822    emit(StackOp::Push(PushValue::Int(1)));
823    emit(StackOp::Opcode("OP_SUB".into()));
824    emit(StackOp::Opcode("OP_SPLIT".into()));
825    // Stack: [x_bytes, y_prefix, last_byte]
826    emit(StackOp::Opcode("OP_BIN2NUM".into()));
827    emit(StackOp::Push(PushValue::Int(2)));
828    emit(StackOp::Opcode("OP_MOD".into()));
829    // Stack: [x_bytes, y_prefix, parity]
830    emit(StackOp::Swap);
831    emit(StackOp::Drop); // drop y_prefix
832    // Stack: [x_bytes, parity]
833    emit(StackOp::If {
834        then_ops: vec![StackOp::Push(PushValue::Bytes(vec![0x03]))],
835        else_ops: vec![StackOp::Push(PushValue::Bytes(vec![0x02]))],
836    });
837    // Stack: [x_bytes, prefix_byte]
838    emit(StackOp::Swap);
839    emit(StackOp::Opcode("OP_CAT".into()));
840}
841
842/// ecMakePoint: (x: bigint, y: bigint) -> Point.
843/// Stack in: [x_num, y_num] (y on top)
844/// Stack out: [point_bytes (64 bytes)]
845pub fn emit_ec_make_point(emit: &mut dyn FnMut(StackOp)) {
846    // Convert y to 32 bytes big-endian (NUM2BIN(33) to handle sign byte, then take first 32)
847    emit(StackOp::Push(PushValue::Int(33)));
848    emit(StackOp::Opcode("OP_NUM2BIN".into()));
849    emit(StackOp::Push(PushValue::Int(32)));
850    emit(StackOp::Opcode("OP_SPLIT".into()));
851    emit(StackOp::Drop);
852    emit_reverse_32(emit);
853    // Stack: [x_num, y_be]
854    emit(StackOp::Swap);
855    // Stack: [y_be, x_num]
856    emit(StackOp::Push(PushValue::Int(33)));
857    emit(StackOp::Opcode("OP_NUM2BIN".into()));
858    emit(StackOp::Push(PushValue::Int(32)));
859    emit(StackOp::Opcode("OP_SPLIT".into()));
860    emit(StackOp::Drop);
861    emit_reverse_32(emit);
862    // Stack: [y_be, x_be]
863    emit(StackOp::Swap);
864    // Stack: [x_be, y_be]
865    emit(StackOp::Opcode("OP_CAT".into()));
866}
867
868/// ecPointX: extract x-coordinate from Point.
869/// Stack in: [point (64 bytes)]
870/// Stack out: [x as bigint]
871pub fn emit_ec_point_x(emit: &mut dyn FnMut(StackOp)) {
872    emit(StackOp::Push(PushValue::Int(32)));
873    emit(StackOp::Opcode("OP_SPLIT".into()));
874    emit(StackOp::Drop);
875    emit_reverse_32(emit);
876    // Append 0x00 sign byte to ensure unsigned interpretation
877    emit(StackOp::Push(PushValue::Bytes(vec![0x00])));
878    emit(StackOp::Opcode("OP_CAT".into()));
879    emit(StackOp::Opcode("OP_BIN2NUM".into()));
880}
881
882/// ecPointY: extract y-coordinate from Point.
883/// Stack in: [point (64 bytes)]
884/// Stack out: [y as bigint]
885pub fn emit_ec_point_y(emit: &mut dyn FnMut(StackOp)) {
886    emit(StackOp::Push(PushValue::Int(32)));
887    emit(StackOp::Opcode("OP_SPLIT".into()));
888    emit(StackOp::Swap);
889    emit(StackOp::Drop);
890    emit_reverse_32(emit);
891    // Append 0x00 sign byte to ensure unsigned interpretation
892    emit(StackOp::Push(PushValue::Bytes(vec![0x00])));
893    emit(StackOp::Opcode("OP_CAT".into()));
894    emit(StackOp::Opcode("OP_BIN2NUM".into()));
895}
896
897// ===========================================================================
898// Utility: encode 1 << n as a Bitcoin script number
899// ===========================================================================
900
901/// Encode (1 << n) as a Bitcoin Script number (little-endian sign-magnitude).
902/// This matches what the TS emitter produces for `PushValue::Int(bigint)`.
903/// Used for the scalar bit extraction divisor in ecMul where shift amounts
904/// can exceed i128 range.
905fn script_number_pow2(n: usize) -> Vec<u8> {
906    // Script number for 2^n:
907    // - The value 2^n has bit n set and all other bits zero.
908    // - In little-endian: byte index = n/8, bit within byte = n%8.
909    // - Need (n/8)+1 bytes minimum.
910    // - If the highest bit of the last byte is set (bit 7), we need an
911    //   extra 0x00 byte for the sign (positive).
912    let byte_idx = n / 8;
913    let bit_pos = n % 8;
914    let min_len = byte_idx + 1;
915    let needs_sign_byte = bit_pos == 7;
916    let total_len = if needs_sign_byte { min_len + 1 } else { min_len };
917
918    let mut bytes = vec![0u8; total_len];
919    bytes[byte_idx] = 1 << bit_pos;
920    // If bit_pos == 7, the high bit of the last data byte is set,
921    // and we've already added a 0x00 sign byte at the end.
922    bytes
923}