|
| 1 | +import { |
| 2 | + constructJubjubPoint, |
| 3 | + type JubjubPoint, |
| 4 | +} from '@midnight-ntwrk/compact-runtime'; |
| 5 | +import { describe, expect, it } from 'vitest'; |
| 6 | +import { pureCircuits } from '../../../artifacts/MockCurveOps/contract/index.js'; |
| 7 | + |
| 8 | +// --------------------------------------------------------------------------- |
| 9 | +// RUNTIME-INVARIANTS REGRESSION TEST. |
| 10 | +// |
| 11 | +// The ElGamal module relies on every JubjubPoint reaching a curve operation |
| 12 | +// being in the prime-order subgroup. The Midnight runtime guarantees this at |
| 13 | +// the ZK-constraint level: the embedded-curve gadget (midnight-circuits |
| 14 | +// `ecc::native::edwards_chip`) assigns points via cofactor clearing. It |
| 15 | +// constrains the witnessed coordinates to a cofactor-cleared point, whose image |
| 16 | +// is exactly the prime-order subgroup, with `q_mem`'s membership gate |
| 17 | +// (-x^2 + y^2 = 1 + d*x^2*y^2) enforcing on-curve. So a point outside the |
| 18 | +// subgroup (off-curve, low-order, or mixed-order) makes the circuit |
| 19 | +// unsatisfiable, surfacing here as a runtime trap. |
| 20 | +// |
| 21 | +// This test pins that property. If a future runtime stops enforcing it, the |
| 22 | +// trap-expectations below fail loudly as a signal that the module's check-free |
| 23 | +// reliance on subgroup membership must be revisited. |
| 24 | +// --------------------------------------------------------------------------- |
| 25 | + |
| 26 | +// Jubjub base field modulus q = BLS12-381 scalar field order. |
| 27 | +const Q = |
| 28 | + 52435875175126190479447740508185965837690552500527637822603658699938581184513n; |
| 29 | + |
| 30 | +// (0, q-1) = (0, -1): on-curve, order 2 -> NOT in the prime-order subgroup. |
| 31 | +const ORDER_2 = constructJubjubPoint(0n, Q - 1n); |
| 32 | +// (1, 1): off-curve (fails the twisted Edwards equation). |
| 33 | +const OFF_CURVE = constructJubjubPoint(1n, 1n); |
| 34 | +// arbitrary coords < q, unknown structure. |
| 35 | +const GARBAGE = constructJubjubPoint(12345n, 67890n); |
| 36 | + |
| 37 | +// Classify a runtime call as a returned value or a trap. |
| 38 | +const classify = <T>( |
| 39 | + fn: () => T, |
| 40 | +): { ok: true; value: T } | { ok: false; err: string } => { |
| 41 | + try { |
| 42 | + return { ok: true, value: fn() }; |
| 43 | + } catch (e) { |
| 44 | + return { ok: false, err: (e as Error).message }; |
| 45 | + } |
| 46 | +}; |
| 47 | + |
| 48 | +const threw = (p: JubjubPoint, fn: (p: JubjubPoint) => unknown): boolean => |
| 49 | + !classify(() => fn(p)).ok; |
| 50 | + |
| 51 | +describe('JubjubPoint subgroup enforcement (runtime invariant)', () => { |
| 52 | + const inSubgroup = pureCircuits.genMul(5n); // generator-derived -> in subgroup |
| 53 | + |
| 54 | + // MIXED-ORDER point of order 2*ℓ: an in-subgroup point plus the order-2 point. |
| 55 | + // In twisted Edwards, (x,y) + (0,-1) = (-x,-y), so this is just coordinate |
| 56 | + // negation of a real point. It is on-curve, NOT in the prime-order subgroup, |
| 57 | + // and (crucially) NOT a low-order point, so it should not hit any |
| 58 | + // addition-formula exception. This is the realistic attack vector. |
| 59 | + const MIXED = constructJubjubPoint(Q - inSubgroup.x, Q - inSubgroup.y); |
| 60 | + |
| 61 | + it('FACT: a JubjubPoint is fabricable from arbitrary coordinates', () => { |
| 62 | + expect(ORDER_2).toEqual({ x: 0n, y: Q - 1n }); |
| 63 | + }); |
| 64 | + |
| 65 | + // ------------------------------------------------------------------------- |
| 66 | + // MIXED-ORDER point: the case that distinguishes "real subgroup enforcement" |
| 67 | + // from "incidental low-order formula exception". |
| 68 | + // ------------------------------------------------------------------------- |
| 69 | + describe('mixed-order point (order 2*ℓ) — the decisive case', () => { |
| 70 | + it('TRAPS ecMul on a mixed-order point (genuine subgroup enforcement)', () => { |
| 71 | + expect(classify(() => pureCircuits.doEcMul(MIXED, 3n)).ok).toBe(false); |
| 72 | + }); |
| 73 | + |
| 74 | + it('TRAPS ecAdd on a mixed-order point', () => { |
| 75 | + expect(classify(() => pureCircuits.doEcAdd(MIXED, inSubgroup)).ok).toBe( |
| 76 | + false, |
| 77 | + ); |
| 78 | + }); |
| 79 | + }); |
| 80 | + |
| 81 | + // ------------------------------------------------------------------------- |
| 82 | + // ecMul: does it reject non-subgroup / off-curve points? |
| 83 | + // ------------------------------------------------------------------------- |
| 84 | + describe('ecMul input validation', () => { |
| 85 | + it('accepts an in-subgroup point', () => { |
| 86 | + expect(threw(inSubgroup, (p) => pureCircuits.doEcMul(p, 3n))).toBe(false); |
| 87 | + }); |
| 88 | + |
| 89 | + it('TRAPS on an on-curve order-2 point (off-subgroup)', () => { |
| 90 | + expect(threw(ORDER_2, (p) => pureCircuits.doEcMul(p, 3n))).toBe(true); |
| 91 | + }); |
| 92 | + |
| 93 | + it('TRAPS on an off-curve point', () => { |
| 94 | + expect(threw(OFF_CURVE, (p) => pureCircuits.doEcMul(p, 3n))).toBe(true); |
| 95 | + }); |
| 96 | + |
| 97 | + it('TRAPS on a garbage point', () => { |
| 98 | + expect(threw(GARBAGE, (p) => pureCircuits.doEcMul(p, 3n))).toBe(true); |
| 99 | + }); |
| 100 | + }); |
| 101 | + |
| 102 | + // ------------------------------------------------------------------------- |
| 103 | + // ecAdd: THE linchpin for encryptPoint (m flows through ecAdd, not ecMul). |
| 104 | + // ------------------------------------------------------------------------- |
| 105 | + describe('ecAdd input validation', () => { |
| 106 | + it('accepts two in-subgroup points', () => { |
| 107 | + expect( |
| 108 | + classify(() => pureCircuits.doEcAdd(inSubgroup, inSubgroup)).ok, |
| 109 | + ).toBe(true); |
| 110 | + }); |
| 111 | + |
| 112 | + it('TRAPS when an order-2 point is added', () => { |
| 113 | + expect(classify(() => pureCircuits.doEcAdd(ORDER_2, inSubgroup)).ok).toBe( |
| 114 | + false, |
| 115 | + ); |
| 116 | + }); |
| 117 | + |
| 118 | + it('TRAPS when an off-curve point is added', () => { |
| 119 | + expect( |
| 120 | + classify(() => pureCircuits.doEcAdd(OFF_CURVE, inSubgroup)).ok, |
| 121 | + ).toBe(false); |
| 122 | + }); |
| 123 | + |
| 124 | + it('TRAPS when a garbage point is added', () => { |
| 125 | + expect(classify(() => pureCircuits.doEcAdd(GARBAGE, inSubgroup)).ok).toBe( |
| 126 | + false, |
| 127 | + ); |
| 128 | + }); |
| 129 | + }); |
| 130 | +}); |
0 commit comments