Add ecNeg for JubJub point negation#269
Add ecNeg for JubJub point negation#269adamreynolds-io wants to merge 5 commits intoLFDT-Minokawa:mainfrom
Conversation
Plugin Test Summary 1 files 3 suites 1s ⏱️ Results for commit 8e35a42. ♻️ This comment has been updated with latest results. |
Implement elliptic curve point negation as a new native function in the Compact standard library. On the JubJub twisted Edwards curve, negation of (x, y) is (-x, y). Compiler: compose from existing ZKIR instructions (encode → neg → decode) rather than adding a new instruction to the proof system. In ZKIRv2, the point is already flattened to (x, y) fields, so we negate x and bind y. Runtime: pure field math (-x mod p, y), consistent with the existing pattern where simple coordinate operations (jubjubPointX, jubjubPointY, constructJubjubPoint) don't delegate to onchain-runtime. Closes LFDT-Minokawa#166 Signed-off-by: Adam Reynolds <adam.reynolds@shielded.io> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cfb96e5 to
8831917
Compare
Update the standard library API docs, runtime README, and changelog for the new ecNeg (ec_neg) function. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Adam Reynolds <adam.reynolds@shielded.io>
Compactc E2E Test Summary 1 files ± 0 1 suites - 47 5m 10s ⏱️ + 2m 41s Results for commit 8e35a42. ± Comparison against base commit 90d3f41. This pull request removes 467 and adds 2828 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Add ec_neg/ecNeg calls to the standard library example contracts alongside the existing ecAdd, ecMul, and ecMulGenerator usage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Adam Reynolds <adam.reynolds@shielded.io>
compiler/midnight-natives.ss
Outdated
|
|
||
| (declare-native-entry circuit ecNeg | ||
| "__compactRuntime.ecNeg" | ||
| ([a (TypeRef JubjubPoint) (discloses "the negation of")]) |
There was a problem hiding this comment.
To match the style of the existing disclosure failure messages, this should be: "the elliptic curve negation of".
compiler/standard-library-aliases.ss
Outdated
| (degrade_to_transient . degradeToTransient) | ||
| (upgrade_from_transient . upgradeFromTransient) | ||
| (ec_add . ecAdd) | ||
| (ec_neg . ecNeg) |
There was a problem hiding this comment.
There's no reason for this, remove it.
This support allows a programmer to write ec_neg instead of ecNeg and then run compact fix to change it to ecNeg. But nobody would do that in new code, they'd just write the real name. Fixups are intended for existing code.
compiler/test.ss
Outdated
| '( | ||
| "import CompactStandardLibrary;" | ||
| "" | ||
| "ledger forceField: Field; circuit forceProof(): [] { forceField = 7; }" |
There was a problem hiding this comment.
This complicated setup doesn't really make a difference here. I just write:
ledger impure: Boolean;
export circuit test(...): ... {
impure = true;
}
There was a problem hiding this comment.
Switched to the simpler ledger impure: Boolean; impure = true; pattern.
| const test_21 = upgradeFromTransient(1); | ||
| const test_22 = default<JubjubPoint>; | ||
| const test_23 = ecAdd(default<JubjubPoint>, default<JubjubPoint>); | ||
| const test_23b = ecNeg(default<JubjubPoint>); |
There was a problem hiding this comment.
Get rid of this line (see below).
| const test_21 = upgradeFromTransient(1); | ||
| const test_22 = default<JubjubPoint>; | ||
| const test_23 = ecAdd(default<JubjubPoint>, default<JubjubPoint>); | ||
| const test_23b = ecNeg(default<JubjubPoint>); |
There was a problem hiding this comment.
I don't know why the tests are structured this way, but this duplicates the coverage in external/examples.compact and isn't necessary here.
This test is supposedly testing that the output of compact fix can be compiled, and nobody will have code using ec_neg at all, nor will they have code using ecNeg(default<NativePoint>). So I'd get rid of it.
runtime/test/stdlib.test.ts
Outdated
| test('elliptic curve negation', () => { | ||
| const g = compactRuntime.ecMulGenerator(1n); | ||
| const neg_g = compactRuntime.ecNeg(g); | ||
| // g + neg(g) should equal the identity point (0, 1) |
There was a problem hiding this comment.
I'm clearly out of my area of expertise, but doesn't g - g equal zero (not the identity)?
There was a problem hiding this comment.
On twisted Edwards curves the identity element is (0, 1), which is what ecAdd(g, ecNeg(g)) returns. The test confirms this. "Zero" and "identity" are the same thing here — the additive identity of the group.
runtime/src/built-ins.ts
Outdated
| } | ||
|
|
||
| /** | ||
| * The Compact builtin `ec_neg` function |
There was a problem hiding this comment.
Oh, it's really unfortunate that the comment uses the old spelling for all these. I'd change this to ecNeg to stop contributing to the problem :)
6af155b to
8b111d4
Compare
runtime/test/stdlib.test.ts
Outdated
| const g = compactRuntime.ecMulGenerator(1n); | ||
| const neg_g = compactRuntime.ecNeg(g); | ||
| // neg negates x, preserves y | ||
| expect(neg_g.x).not.toEqual(g.x); |
There was a problem hiding this comment.
Is there a stronger statement to be made here?
There was a problem hiding this comment.
Good point — we can assert the exact negation rather than just "not equal":
const FIELD_MODULUS = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001n;
expect(neg_g.x).toEqual(FIELD_MODULUS - g.x);The double-negation and identity tests already cover the algebraic properties, but checking the exact field arithmetic makes this less fragile. Will update.
- Fix instruction order in ZKIRv3 (cons* accumulates reversed list) - Fix disclosure message to "the elliptic curve negation of" - Remove ec_neg alias (no fixup support needed for new functions) - Simplify test boilerplate (ledger impure: Boolean pattern) - Bump toolchain to 0.30.105, language to 0.22.102 - Fix CHANGELOG format to match project convention - Remove unnecessary ecNeg from camelCase examples - Fix runtime test assertions (neg negates x, preserves y) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Adam Reynolds <adam.reynolds@shielded.io>
8b111d4 to
bbcb3ee
Compare
Assert neg_g.x equals FIELD_MODULUS - g.x instead of just checking inequality, per gilescope's review feedback. Signed-off-by: Adam Reynolds <adam.reynolds@shielded.io> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Pushed a fix strengthening the All review comments have been addressed. The ZKIRv2 and print-typescript tests Kevin requested are already in the previous push. The @kmillikin ready for re-review when you get a chance — the outstanding threads are all either outdated or the |
Summary
ecNeg/ec_negas a native function for JubJub point negationencode→neg→decode) — no proof system changes neededneg(x, y) = (-x mod p, y)Closes #166
Design Decisions
The following assumptions were made during implementation and should be validated by reviewers:
Scope:
ecNegonly, noecSub— Users can compose subtraction themselves viaecAdd(a, ecNeg(b)). Adding a separateecSubconvenience function was considered but deferred to keep the change minimal. If a stdlib-levelecSubcircuit is desired, it can be added as a follow-up without any native/ZKIR changes.ZKIR approach: compose existing instructions, not a new Lzkir instruction — The proof system (midnight-zk) has
EccInstructions::negate()but it's not exposed as a standalone ZKIRv3 instruction inmidnight-ledger/zkir-v3/src/ir.rs. Rather than adding a new instruction to the ledger (which would require cross-repo changes), we compose from three existing instructions:encode(extract x, y) →neg(negate x) →decode(reconstruct point). This costs 3 ZKIR instructions per negation. If a dedicatedEcNeginstruction is later added to ir.rs, the compiler can be updated to emit a single instruction — same semantics, fewer constraints.Runtime: pure field math, no
ocrtdelegation — The onchain-runtime-v3 does not exportecNeg. Rather than blocking on an ocrt update, the runtime implements negation as(-x mod p, y)directly in TypeScript. This is consistent with howjubjubPointX,jubjubPointY, andconstructJubjubPointare implemented — simple coordinate operations use pure TS, while complex curve arithmetic (ecAdd,ecMul) delegates toocrt. JosephDenman'scurves-proposalbranch designs aCurveOpsinterface withneg()that can wrap this in the future.Changes
compiler/midnight-natives.ss(JubjubPoint) -> JubjubPointcompiler/standard-library-aliases.ssec_neg→ecNegaliascompiler/zkir-v3-passes.ssencode→neg→decodecompiler/zkir-passes.ssruntime/src/built-ins.tsecNeg()— pure field mathruntime/test/stdlib.test.tscompiler/test.ssdoc/api/CompactStandardLibrary/exports.mdecNegdoc/api/CompactStandardLibrary/README.mdruntime/README.mdCHANGELOG.mdexamples/**ecNeg/ec_negusage in 5 example contractsTest plan
🤖 Generated with Claude Code