Skip to content

I3: ec_mul intrinsic — point scalar multiplication (fixes #104, stacks on #84) - #105

Closed
liqdmetal wants to merge 6 commits into
DEROFDN:community-devfrom
liqdmetal:feature/dvm-i3-ecmul
Closed

I3: ec_mul intrinsic — point scalar multiplication (fixes #104, stacks on #84)#105
liqdmetal wants to merge 6 commits into
DEROFDN:community-devfrom
liqdmetal:feature/dvm-i3-ecmul

Conversation

@liqdmetal

@liqdmetal liqdmetal commented Aug 23, 2026

Copy link
Copy Markdown

Summary

I3 from the intrinsic agenda: ec_mul(point_hex String, scalar Uint64) -> String — point scalar multiplication for the DVM, gated >= 9.0.0, 30k gas.

What it does

Multiplies a compressed bn256 G1 point (DERO's 33-byte encoding) by a uint64 scalar, returning the compressed result. The homomorphic counterpart to ec_add (I2, already in DVM v9 PR #84):

Op Property
ec_add(c1, c2) c1 + c2 (commitment accumulation)
ec_mul(c, k) k·c (point derivation, key blinding, commitment scaling)

Together they give a contract complete in-VM group arithmetic on commitments — point derivation for key blinding, commitment scaling, and accumulation — without oracle dependence.

Tests (TestEcMul)

  • ec_mul(c, 2) == ec_add(c, c) — the homomorphic pair
  • scalar composition: ec_mul(ec_mul(c, 3), 2) == ec_mul(c, 6)
  • ec_mul(c, 1) == c (identity)
  • ec_mul(c, 0) produces a valid point encoding
  • output is always a 33-byte compressed point (66 hex)
  • version gate: invisible to 1.2.3 contracts

Hardening (wargame finding, now in the code)

Strict point decoding — strictDecodeG1. ec_mul (and its siblings ec_add, verify_commit in PR #84) decode the caller-supplied compressed point with DecodeCompressed + err == nil. The conformance PR (#86) documented that Go's decoder accepts x ≥ p encodings — it computes y from x mod p but stores the raw x. So a contract could feed an off-curve (x ≥ p) point and the intrinsic would compute on it, while a strict decoder (the clean-room Rust port) rejects the encoding → the two implementations diverge on contract behavior → chain-split class bug.

strictDecodeG1 now validates x < p (canonical field encoding) before DecodeCompressed, applied to ec_mul, ec_add, and verify_commit. Non-canonical input panics (recovered → deterministic tx failure), matching the strict decoder. Canonical points are unaffected.

Pinned by TestWargameEcMulAcceptsOffCurvePoint (proves the fix: off-curve input now rejected, canonical input still accepted) and in the derohe-rs differential harness (strict_point_decode, 8 vectors).

Relationship


Branch: feature/dvm-i3-ecmul in the fork liqdmetal/derohe-improvements-by-liqdmetal (stacked on feature/dvm-v9-intrinsics).

…mit, asset_balance, ec_add)

Five new DVM-BASIC intrinsics (gated semver >= 9.0.0, so existing
contracts are unaffected — the func_table Range mechanism):

- verify_sig(pubkey_hex, message, sig_hex) -> Uint64: Ed25519 in-VM
  signature verification. Enables anonymous contract authorization
  (caller proves key ownership in encrypted SCDATA at ringsize >= 4,
  no SIGNER()/ringsize-2) — the K0 Fix C path. Gas 250k. Stdlib, no
  new deps.
- hash_to_point(input) -> String: HashToPoint(HashtoNumber(input)),
  33-byte compressed G1 hex, deterministic across nodes. Pinned to the
  protocol generator derivation (algebra_pedersen.go). Gas 30k.
- pedersen_commit(value, blind_hex) -> String + verify_commit(value,
  blind_hex, commit_hex) -> Uint64: v*G + r*H with NUMS H (hash-to-point
  of PROTOCOL_CONSTANT+H), 32-byte blind (256-bit hiding). Commit
  on-chain, reveal off-chain, SC verifies — no oracle trust. Gas 45k.
- asset_balance(asset_hex) -> Uint64: reads the SC's OWN stored balance
  for any asset (incl. DERO) via BalanceLoader(scid, asset). Closes the
  gap where derovalue()/assetvalue() only report the current tx's
  incoming value. Gas 2k.
- ec_add(p1_hex, p2_hex) -> String: homomorphic accumulation of
  compressed G1 points — ec_add(c1,c2) == pedersen_commit(v1+v2, b1+b2),
  enabling commitment-state updates (e.g. AMM reserves) without
  revealing deltas. Gas 15k.

Tests (dvm/verify_sig_test.go): 6 test functions covering valid/tampered/
malformed inputs, determinism, version gate, the homomorphic property,
and BalanceLoader wiring.

Consensus note: new intrinsics change VM state output -> hard fork
(DVM version bump). The version gate means old contracts keep running
unchanged. This is the foundation for K0 Fix C and confidential
settlement.

Also carries the build-manifest fix (go.mod/go.sum): the current tree
does not build from a fresh clone.
Regenerates vendor/ via go mod tidy + go mod vendor (adds missing
vendor/modules.txt) so the tree builds from a fresh clone without
-mod=mod. Deps unchanged.
community-dev base calls l.Operation.KickReader(), which no published
chzyer/readline implements (v1.5.1) -> wallet-cli fails to compile from a
fresh clone. Replace with the same UI shim used on the main fork branch:
_ = l.Operation (read-unblock helper, wallet-only, not consensus).
ec_mul(point_hex String, scalar Uint64) -> String (33-byte compressed
point, hex), gated >= 9.0.0, 30k gas.

The homomorphic counterpart to ec_add (I2): combined, a contract can
build and accumulate commitments entirely in-VM — point derivation,
key blinding, commitment scaling. Completes the group-arithmetic pair
the DVM v9 package started.

TestEcMul verifies: ec_mul(c,2) == ec_add(c,c); scalar composition
(3*2 == 6); ec_mul(c,1) == c; ec_mul(c,0) valid encoding; output is
always a 33-byte compressed point; version gate (1.2.3 rejected).
ec_mul decodes its compressed-point input with DecodeCompressed and only
checks err == nil. bn256/changes.go computes y from x mod p (xToY) but
stores the RAW x — so an x>=p encoding decodes 'successfully' to an
on-curve point (x reduced) where a strict decoder REJECTS the encoding.
The intrinsic therefore accepts x>=p input and computes a deterministic
result, while the clean-room Rust port would reject at decode -> the two
implementations diverge on contract behavior -> chain-split class bug
(the exact class the conformance PR documents).

Pinned: ec_mul(x_gt_p, 2) returns a result (point-at-infinity encoding)
instead of rejecting. Fix direction: strict x < p validation after
decode in ec_mul/ec_add/verify_commit so every implementation accepts
and rejects identically.
Wargame finding pinned by TestWargameEcMulAcceptsOffCurvePoint: the point
intrinsics decoded caller-supplied compressed points with
DecodeCompressed + err==nil, which accepts x>=p encodings (bn256/changes.go
computes y from x mod p but stores the raw x). The intrinsic then computed
on the point where a strict decoder (clean-room Rust) rejects the encoding
-> implementations diverge on contract behavior -> chain-split class bug.

Fix: strictDecodeG1 validates x < p (canonical field encoding) before
DecodeCompressed, used by ec_mul, ec_add, and verify_commit. Off-curve /
non-canonical input now panics (recovered -> deterministic tx failure),
matching the strict decoder. Canonical points unaffected; full dvm suite
green.
@liqdmetal

Copy link
Copy Markdown
Author

Superseded by PR #128 — the consolidated intrinsics package. Same code, one reviewable PR with no vendor noise.

@liqdmetal liqdmetal closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant