Implement OP_ECADD, OP_ECMUL, OP_ECPAIRING#79
Conversation
…/otel and CVE-2026-33811, -33814, -39820, -39836, -42499 in the Go stdlib shipped via the Dockerfile builder image.
There was a problem hiding this comment.
🔍 Arkana Code Review — OP_ECADD / OP_ECMUL / OP_ECPAIRING
Verdict: Request Changes — protocol-critical consensus opcodes require human sign-off before merge. The implementation is high quality overall, but there are issues to address and downstream coordination required.
⚠️ PROTOCOL-CRITICAL — REQUIRES HUMAN REVIEW
This PR adds three new consensus opcodes to the Arkade Script VM. Any implementation divergence between nodes causes chain forks. This must not merge without explicit human sign-off from protocol leads.
Cross-Repo Breakage — Action Required
1. rust-sdk must be updated (blocking)
ark-script/src/opcodes.rs defines ARKADE_OPCODES ending at 0xf5. The comment reads:
"Arkade opcodes occupy byte values
0xb3and0xc4..=0xf5"
Any Rust consumer using ARKADE_OPCODES to validate scripts will reject 0xf6–0xf8 as unknown. This needs a coordinated update — ideally an open PR on rust-sdk before this merges.
2. Downstream Go consumers: bancod and layerzero-usdt0-arkade-demo both import github.com/ArkLabsHQ/introspector/pkg/arkade. They will pick up these opcodes on next go get. No breakage expected (new opcodes only, no API changes), but worth a heads-up.
Findings
1. [Medium] No activation gate for new opcodes
pkg/arkade/opcode.go:609-611 — The three opcodes go from opcodeInvalid → live handlers with no version/height/flag gate. If any node on the network is running the old code when a script uses OP_ECADD, it will reject the transaction as invalid while updated nodes accept it — chain split.
Is there an activation mechanism outside this repo (e.g., in arkd)? If not, this needs one, or the deployment plan needs to guarantee all nodes upgrade atomically.
2. [Low] crypto/elliptic.P256() Add/ScalarMult deprecation risk
pkg/arkade/ec_ops.go:627-657 — The deprecated crypto/elliptic API is used for secp256r1. The //nolint:staticcheck suppression is acknowledged in the comments, which is good. However, Go could change or remove the deprecated methods in a future release. Consider documenting the minimum Go version constraint, or migrating to crypto/ecdh with manual point serialization if feasible.
3. [Low] Missing edge-case test vectors
The test suite is solid but a few boundary vectors are missing:
k = order - 1for each curve (last valid scalar — confirms the boundary check is<not<=)pair_count = 16(exactly atmaxECPairingCount— confirms the boundary is>not>=)OP_ECMULwithsecp256r1infinity input (P=(0,0), k>0) — tested for secp256k1 but not P-256OP_ECADDwithsecp256r1infinity + infinity — tested for secp256k1 but not P-256
These are consensus-critical boundaries — a fence-post error here means valid scripts fail or invalid scripts succeed.
4. [Info] Pairing CPU bound
maxECPairingCount = 16 is reasonable. BN254 pairing-product check with 16 pairs on gnark-crypto should complete in ~50-100ms. Worth benchmarking and documenting the expected worst-case latency to justify the cap in the spec.
What Looks Good ✅
- On-curve validation on all inputs before computation — prevents invalid-point attacks
- G2 subgroup check (
IsInSubGroup()) for BN254 pairing — critical for soundness, correctly implemented - Field/scalar bounds checking —
popInFieldElementenforces0 ≤ n < modulus,popInGroupScalarenforces0 ≤ k < order - Minimal BigNum encoding enforced via existing
PopBigNum— no non-canonical encoding ambiguity - Infinity point handling —
(0,0)convention consistently applied across all three curves with explicit special-casing for P-256'scrypto/ellipticwhich rejects(0,0)fromIsOnCurve - Fuzz test builders (
ecCurveCaseBuilder,ecPairingFuzzCaseBuilder) — well-designed to penetrate past validation gates - gnark-crypto v0.19.2 pinned — correct for consensus-critical code
- Error codes — all failures map to documented consensus error codes
- Stack discipline — pop/push order verified against spec; alt-stack and cond-stack invariants checked in property tests
- Pairing follows EIP-197 semantics — empty set returns true, coordinate order matches, but with stricter scalar validation (good deviation from EIP-196)
- Comprehensive test coverage — off-curve, out-of-field, negative values, non-minimal encoding, subgroup violations all tested
Summary
The EC arithmetic implementation is well-engineered and the security-critical checks (on-curve, subgroup, field bounds) are all present. The main concerns are:
- Activation gating — how do we prevent chain splits during rollout?
- rust-sdk coordination — needs
0xf6-0xf8added toARKADE_OPCODES - Missing boundary test vectors —
k = order-1andpair_count = 16
Once the activation strategy is clarified and the missing tests are added, this is ready for human protocol-lead sign-off.
🤖 Reviewed by Arkana · aggressive code review · model: opus
louisinger
left a comment
There was a problem hiding this comment.
concept ACK but we need a strategy for the liquid opcodes because they are confusing. I have the feeling we can "drop" them and replace by these new primitives ?
Summary
Adds three Arkade Script opcodes for elliptic curve arithmetic, selectable at runtime by
curve_id:OP_ECADD0xf6x1 y1 x2 y2 curve_idx3 y3OP_ECMUL0xf7x y k curve_idx2 y2OP_ECPAIRING0xf8[g1_x g1_y g2_x_c1 g2_x_c0 g2_y_c1 g2_y_c0]… pair_count curve_idboolCurves at launch:
secp256k1secp256r1(NIST P-256)alt_bn128/ BN2540xf9stays reserved. Pairing only works foralt_bn128at launch; the curve_id argument is kept so a future BLS12 curve can reuse the same opcode shape.Stack interface
All numeric inputs and outputs — curve IDs, pair counts, affine coordinates, Fp2 components, scalars, result coordinates — are Arkade BigNums (minimally encoded sign-magnitude little-endian). A script can build a coordinate with
OP_ADD/OP_SUBand feed it directly intoOP_ECADDwithout an extra fixed-width encoding step.(0, 0)is the wire representation of the G1 point at infinity. G2 infinity is all fourFp2components zero.OP_ECPAIRINGfollows EIP-197 coordinate order foralt_bn128G2: each pair is six BigNums(g1_x, g1_y, g2_x_c1, g2_x_c0, g2_y_c1, g2_y_c0).k = 0forOP_ECMULreturns infinity.pair_count = 0forOP_ECPAIRINGreturns true (matches EIP-197).0 ≤ k < group_order. Coordinates must be0 ≤ x < field_modulus. This intentionally differs from EIP-196, where the scalar is a 32-byte calldata blob that can exceed the group order — Arkade Script has native arbitrary-precision arithmetic, so we reject non-canonical scalars at the opcode boundary.Validation and failure model
Every structural or value error fails script execution; only a valid pairing-product that happens to evaluate to a non-identity element pushes canonical false. Failure cases:
curve_idalt_bn128supported at launch)≥ field_modulus≥ group_orderalt_bn128r-subgroup (explicit subgroup check; G1 has cofactor 1 so on-curve suffices)pair_count > 16Errors map to
txscript.ErrInvalidStackOperationexcept where existing stack/BigNum helpers already produce a more specific code (ErrMinimalData,ErrNumberTooBig, stack-underflowErrInvalidStackOperation).pair_countis hard-capped at 16. Arkade Script has no gas model, so pairing CPU work must be bounded deterministically.Implementation
All curve logic and adapter code lives in
pkg/arkade/ec_ops.go:supportsPairing) in one table.*big.Intvalidation helpers at the opcode boundary (popCurveID,popInFieldElement,popInGroupScalar).opcodeECAdd,opcodeECMul,opcodeECPairing.Backends:
alt_bn128(G1, G2, pairing) andsecp256k1(G1) —github.com/consensys/gnark-cryptov0.19.2 (pinned).secp256r1/ NIST P-256 — Go standard librarycrypto/elliptic.P256()(the samecrypto/internal/nistecimplementation used bycrypto/tls,crypto/ecdsa, andcrypto/ecdh).