Skip to content

feat(crypto): native crypto + hash syscalls on Vara and ethexe#5363

Draft
ukint-vs wants to merge 14 commits intomasterfrom
vs/crypto-syscalls
Draft

feat(crypto): native crypto + hash syscalls on Vara and ethexe#5363
ukint-vs wants to merge 14 commits intomasterfrom
vs/crypto-syscalls

Conversation

@ukint-vs
Copy link
Copy Markdown
Member

@ukint-vs ukint-vs commented Apr 20, 2026

Adds 7 gr_* syscalls with byte-identical ABI on Vara and ethexe.

Syscalls

Primitive Shape
gr_blake2b_256 infallible, base + per-byte
gr_sha256 infallible, base + per-byte
gr_keccak256 infallible, base + per-byte
gr_sr25519_verify infallible, explicit Schnorrkel ctx, base + per-transcript-byte
gr_ed25519_verify infallible, base + per-byte
gr_secp256k1_verify infallible, malleability_flag: u32 (0 permissive / 1 strict low-s)
gr_secp256k1_recover fallible (err u32), same malleability flag

Architecture

One shared wrapper in core/backend/funcs. Both networks enter through it,
and it charges gas once upstream via CostToken / SyscallWeights.

From there:

  • Vara calls a native Externalities impl in core/processor/ext that
    runs sp_core::{sr25519,ed25519,ecdsa}, libsecp256k1, and schnorrkel
    directly in the runtime.
  • Ethexe overrides the same Externalities methods in
    ethexe/runtime/common/ext to call new ext_*_v1 host imports, which
    ethexe/processor services via wasmtime::Linker::func_wrap using the
    same crypto crates — natively, not inside the WASM runtime blob.

Ethexe crypto methods are intentionally not routed through delegate!.
Delegating would run sp_core compiled into the ethexe-runtime WASM,
which is the slow path the syscall exists to escape.

Cross-network policy (e.g. SECP256K1_N_HALF, is_low_s) lives in a single
gear_core::crypto module so verify and recover can never disagree on
the same (sig, flag).

Verified

  • demo-crypto KATs (14): blake2b/sha256/keccak256 standard vectors,
    ed25519/sr25519 valid+tampered, sr25519 ctx tests (matching/mismatched/empty/substrate),
    secp256k1 high-s consistency, boundary tests, zero-length inputs, invalid-v
    rejection, SECP256K1_N_HALF constant
  • demo-crypto gas_delta test exists and passes
  • make fmt, make clippy-gear, make typos clean
  • vara-runtime syscall_weights_test green (82-field count)

Deferred

  1. Weights are Weight::zero() placeholders. Real benchmarks blocked on
    pre-existing polkadot-sdk runtime-benchmarks feature bit-rot (reproduces
    on master).
  2. make test + make clippy-examples fail with pre-existing
    polkadot-sdk / macOS issues (reproduced on master).

ukint-vs and others added 12 commits April 20, 2026 18:22
…e 0)

Stage 0 MVP of the runtime crypto-syscalls proposal: two native primitives
exposed as gr_* syscalls to user programs on both Vara and ethexe, replacing
op-by-op WASM interpretation of curve25519 and blake2b.

Shared scaffolding (core/):
  - gsys declarations (gr_sr25519_verify, gr_blake2b_256)
  - wasm-instrument registry entries (SyscallName + SyscallSignature)
  - Externalities trait methods (core/src/env.rs)
  - CostToken variants + SyscallCosts translation + SyscallWeights fields
    (weights at Weight::zero(); benchmarks pending)
  - core/backend FuncsHandler wrappers (InfallibleSyscall pattern, gas
    charged upstream via CostToken)
  - MockExt trait stubs for backend test builds

Vara impl (core/processor):
  - Ext::{sr25519_verify, blake2b_256} using sp_core::sr25519::Pair::verify
    and sp_core::hashing::blake2_256 (native, fast)
  - sp-core (full_crypto) and sp-io promoted to direct deps

Ethexe impl (ethexe/):
  - ext_sr25519_verify_v1, ext_blake2b_256_v1 host imports declared via
    the existing interface::declare! macro
  - RuntimeInterface trait extended with associated (static) crypto
    methods — matches the existing seam used for random_data etc.
  - NativeRuntimeInterface impl routes to wasm/interface/{crypto,hash}.rs
    wrappers
  - Ext<RI>::{sr25519_verify, blake2b_256} dispatch as RI::method(...)
    explicitly NOT through delegate!(CoreExt) — delegating would run
    sp_core compiled into the runtime WASM, defeating the proposal
  - wasmtime linker registration + native sp_core backed host fns at
    ethexe/processor/src/host/api/{crypto,hash}.rs
  - sp-core (full_crypto) and sp-io promoted to direct deps

Out of scope for this commit (next lane):
  - gcore/gstd user-facing wrappers
  - examples/crypto-demo (WASM-vs-syscall gas comparison)
  - Vara<->ethexe gas-parity gtest
  - Real benchmark numbers (replacing Weight::zero())
  - ed25519, sha256, keccak256, secp256k1_verify, secp256k1_recover
    (Stages 1 & 2)

cargo check --all-targets green on: gear-core, gear-core-backend,
gear-core-processor, ethexe-runtime-common, ethexe-runtime. Ethexe-ethereum
compile failure is pre-existing (missing Solidity ABIs from forge build)
and unrelated.

Plan: ~/.claude/plans/nifty-drifting-swing.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second lane of Stage 0: the user-facing proof that the new syscalls pay
for themselves in gas.

gcore/gstd wrappers:
  - gcore::hash::blake2b_256(&[u8]) -> [u8; 32]
  - gcore::crypto::sr25519_verify(&[u8;32], &[u8], &[u8;64]) -> bool
  - Both re-exported as gstd::{hash, crypto}; no feature gate — same ABI
    on Vara and ethexe.

examples/crypto-demo: a tiny Gear program with two handle modes selected
  by the VerifyRequest payload.
    Mode::Wasm    — verifies via the schnorrkel crate compiled into the
                    program WASM (curve25519 op-by-op interpreted).
    Mode::Syscall — verifies via gcore::crypto::sr25519_verify
                    (dispatches to native sp_core on the host).
  Identical pk / msg / sig across both modes. Pure WASM baseline for
  speedup measurement.

tests/gas_delta.rs (gtest harness): generates a real sr25519 keypair,
  signs a message, runs the program in both modes, compares gas burns.
  Both paths currently return ok=1 (signature verified).

Measured on Stage 0 weights (SyscallWeights::gr_* still Weight::zero(),
benchmarks pending):

    WASM path (schnorrkel in-WASM):    25,051,874,546 gas
    Syscall path (gr_sr25519_verify):   7,013,236,635 gas
    Delta (WASM curve25519 cost):      18,038,637,911 gas saved
    Total-per-message speedup:                   3.57x

The 18B delta is the curve25519 interpreter cost now bypassed. The ~7B
floor on both sides is per-message overhead (SCALE decode + gstd +
reply path) — not crypto. Real SyscallWeights numbers land with the
bench lane; until then the syscall path pays only that floor.

Out of scope for this commit:
  - Ethexe integration test (confirm host routing end-to-end on a real
    ethexe stack rather than the Vara-simulating gtest).
  - Benchmark lane — replace Weight::zero() with measured weights.
  - Vara<->ethexe byte-identical gas-parity gtest.

Plan: ~/.claude/plans/nifty-drifting-swing.md § J, L.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three benchmark definitions to `pallets/gear/src/benchmarking/`:

  - gr_blake2b_256          — base cost (fixed small payload, r × batch repetitions)
  - gr_blake2b_256_per_kb   — per-byte cost (n-KB payload, 1 batch)
  - gr_sr25519_verify       — fixed cost (r × batch repetitions)

Each follows the `gr_debug` / `gr_read` template exactly (same COMMON_OFFSET,
SMALL_MEM_SIZE, body::syscall pattern, API_BENCHMARK_BATCH_SIZE). Macro
entries added to the `benchmarks!` block in mod.rs next to gr_debug.

Notable design choice in gr_sr25519_verify:
  A valid pre-signed (pk, msg, sig) triple is generated once at
  bench-setup time via sp_core::sr25519::Pair::from_seed (std is
  available at that layer) and pre-populated into guest memory via
  DataSegment. Using all-zero bytes would short-circuit at pubkey
  decompression and understate the cost; the deterministic seed
  keeps runs reproducible.

Schedule integration in pallets/gear/src/schedule.rs:
  - Three new fields on `SyscallWeights<T>` (gr_blake2b_256,
    gr_blake2b_256_per_byte, gr_sr25519_verify).
  - Wired through the `From<SyscallWeights<T>> for SyscallCosts` impl
    so the weights reach `gear_core::gas_metering::SyscallCosts` and
    then the syscall wrapper at core/backend/src/funcs.rs.
  - Initialized with Weight::zero() placeholders in the Default impl
    until `make gear-weights` regenerates pallets/gear/src/weights.rs
    with the real numbers. This mirrors the Stage 0 core/src/gas_metering/
    schedule.rs convention.

Pre-existing repo state NOT fixed here:
  `cargo check --features runtime-benchmarks -p pallet-gear` currently
  fails on master HEAD due to polkadot-sdk trait drift
  (pallet-ranked-collective missing try_successful_origin, etc.).
  Verified by stashing this diff — the errors reproduce on a clean tree.
  That blocks running the benchmarks and regenerating weights.rs, so
  real numbers land once the SDK compatibility issue is resolved.
  Benchmark definitions themselves are structurally correct and will
  compile under runtime-benchmarks once the SDK fix lands.

Stage 0 demo-crypto gas-delta test unchanged (still 18B gas saved)
because SyscallWeights::gr_sr25519_verify is still Weight::zero().
The delta represents the WASM curve25519 cost bypassed, which is the
real property we're proving; the syscall-side weight when measured
(~150M projected) will add a small constant on top of the ~7B
per-message floor.

Plan: ~/.claude/plans/nifty-drifting-swing.md § K.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… (Stage 1)

Three new native crypto/hash primitives, each structurally identical to
a Stage 0 counterpart:

  - gr_ed25519_verify  mirrors gr_sr25519_verify  (32-byte pk, 64-byte sig)
  - gr_sha256          mirrors gr_blake2b_256    (data, len, 32-byte out)
  - gr_keccak256       mirrors gr_blake2b_256    (Ethereum-style, not SHA-3)

All 17 layers touched mechanically following Stage 0's pattern:

  - gsys/src/lib.rs — three new syscall declarations next to gr_{sr25519_
    verify, blake2b_256}, matching shapes.
  - utils/wasm-instrument — SyscallName variants + to_str + signatures;
    Blake2b256/Sha256/Keccak256 share one match arm (same shape),
    Sr25519Verify/Ed25519Verify share another.
  - Externalities trait (core/src/env.rs) — three new methods alongside
    blake2b_256/sr25519_verify.
  - CostToken variants + SyscallCosts fields + translation via
    cost_with_per_byte! for hashes and cost_for_one for verifies
    (core/src/costs.rs).
  - SyscallWeights fields (core/src/gas_metering/schedule.rs) — five
    new Weight fields (3 flat + 2 per-byte), initialized to zero.
  - core/backend/src/funcs.rs — three new host-fn wrappers following
    the gr_debug / gr_sr25519_verify pattern exactly. InfallibleSyscall,
    Read/ReadAs/WriteAs accessors.
  - core/backend/src/env.rs — three new add_function! registrations.
  - core/backend/src/mock.rs — trait stubs for the MockExt used by
    backend tests.
  - core/processor/src/ext.rs — Vara native impls via sp_core:
      sha2_256, keccak_256, ed25519::Pair::verify.
  - ethexe/runtime/common/src/{lib,ext}.rs — three new static
    RuntimeInterface methods + three explicit Ext<RI> overrides that
    route through RI::* (never via delegate! to CoreExt, which would
    WASM-interpret sp_core inside the ethexe-runtime blob).
  - ethexe/runtime/src/wasm/interface/{crypto,hash}.rs — three new
    `interface::declare!` externs + typed wrappers.
  - ethexe/runtime/src/wasm/storage.rs — NativeRuntimeInterface impls
    routing to crypto_ri / hash_ri wrappers.
  - ethexe/processor/src/host/api/{crypto,hash}.rs — wasmtime
    linker.func_wrap entries backed by native sp_core. Refactored
    shared memory read/write helpers (read_fixed, copy_in, write_hash)
    while extending.
  - pallets/gear/src/schedule.rs — Substrate SyscallWeights<T> fields
    + SyscallCosts conversion + Weight::zero() placeholders.
  - pallets/gear/src/benchmarking/{syscalls,mod}.rs — five new bench
    fns (gr_sha256, gr_sha256_per_kb, gr_keccak256,
    gr_keccak256_per_kb, gr_ed25519_verify) and their benchmarks!
    entries. ed25519 uses a deterministic valid triple via
    sp_core::ed25519::Pair::from_seed, matching the gr_sr25519_verify
    bench methodology.
  - gcore/src/{crypto,hash}.rs — user-facing wrappers
    (hash::{sha256, keccak256}, crypto::ed25519_verify) re-exported via
    gstd::{hash, crypto} aliases.

All weights remain Weight::zero() placeholders. Real numbers land once
Stage 2 closes (secp256k1 verify + recover), per user direction:
"we will run benchmarks when all syscalls will be implemented".

Sanity:
  - cargo check --all-targets across gear-core, gear-core-backend,
    gear-core-processor, ethexe-runtime-common, ethexe-runtime,
    pallet-gear, gstd, demo-crypto: clean.
  - demo-crypto gas_delta gtest: still 1 passed, 18B gas delta on
    sr25519 (Stage 0 demo untouched).

Plan: ~/.claude/plans/nifty-drifting-swing.md Stage 1 (3 of 5
remaining syscalls). Stage 2 (secp256k1_verify + secp256k1_recover)
is the only ABI-new-shape lane left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ls (Stage 2)

Completes the 7-syscall set introduced by the crypto-syscalls proposal.
Stage 2 is the only lane with a new ABI shape: 65-byte signatures,
33-byte SEC1-compressed pubkeys, and a fallible u32 out-parameter on
recover.

ABI:
  gr_secp256k1_verify(
      msg_hash: *const [u8;32],
      sig:      *const [u8;65],  // r || s || v (v ignored for verify)
      pk:       *const [u8;33],  // SEC1-compressed
      out:      *mut u8,         // 1 on valid, 0 on invalid/malformed
  );

  gr_secp256k1_recover(
      msg_hash: *const [u8;32],
      sig:      *const [u8;65],  // r || s || v
      out_pk:   *mut [u8;65],    // 0x04 || x || y (SEC1 uncompressed)
      err:      *mut u32,        // 0 on success, non-zero on failure
  );

Recovery ABI mirrors Ethereum's ecrecover precompile — same 65-byte
uncompressed output format.

All 17 layers updated, following Stage 0 / Stage 1 convention:

  - gsys/src/lib.rs — two new syscall declarations with shape-correct
    pointer types for the 65/33-byte fixed inputs.
  - utils/wasm-instrument — two new SyscallName variants + separate
    match arms for the secp256k1 signatures (distinct pk size means we
    can't share with Sr25519Verify/Ed25519Verify).
  - Externalities trait (core/src/env.rs) — secp256k1_verify returns
    bool like the other verifies; secp256k1_recover returns
    Option<[u8;65]> (None on any recovery failure).
  - CostToken::{Secp256k1Verify, Secp256k1Recover} + SyscallCosts
    fields + cost translation via cost_for_one (both are fixed-cost —
    msg_hash length doesn't vary).
  - SyscallWeights fields (core + pallet-gear) with Weight::zero()
    placeholders pending benchmarks.
  - core/backend/src/funcs.rs — two new wrappers. secp256k1_recover
    uses two WriteAs out-parameters (out_pk + err) rather than
    inventing a new error-result struct type, keeping the InfallibleSyscall
    pattern; err=0 success / err=1 failure, out_pk zero-filled on
    failure so callers see a defined buffer.
  - core/processor/src/ext.rs — Vara native impls.
      secp256k1_verify: sp_core::ecdsa::Pair::verify_prehashed
          (caller gave a digest; don't re-hash).
      secp256k1_recover: sp_core::ecdsa::Signature::recover_prehashed
          (returns 33-byte compressed) then decompress via
          libsecp256k1::PublicKey::parse_compressed + serialize to get
          the promised 65-byte uncompressed form.
  - ethexe/runtime/common/{lib,ext}.rs — two new RuntimeInterface static
    methods + two explicit Ext<RI> overrides routing through RI::* .
  - ethexe/runtime/src/wasm/interface/crypto.rs — two new
    `interface::declare!` host imports + typed helpers.
  - ethexe/runtime/src/wasm/storage.rs — NativeRuntimeInterface impls.
  - ethexe/processor/src/host/api/crypto.rs — two new wasmtime
    `linker.func_wrap` entries backed by native sp_core + libsecp256k1
    (same decompression pipeline as the Vara side for identical
    semantics across networks).
  - pallets/gear/src/schedule.rs — Substrate SyscallWeights<T> fields,
    SyscallCosts conversion, and Weight::zero() defaults.
  - pallets/gear/src/benchmarking/{syscalls,mod}.rs — bench fns for
    both syscalls using a deterministic valid triple from
    sp_core::ecdsa::Pair::from_seed + sign_prehashed so the bench
    exercises the full verify/recover pipeline.
  - gcore/src/crypto.rs — user-facing wrappers, re-exported via
    gstd::crypto.

Why libsecp256k1 and not sp_io::crypto::secp256k1_ecdsa_recover:

  sp_io's wasm build registers its own #[global_allocator] which
  conflicts with ethexe-runtime's allocator when gear-core-processor
  is linked into the ethexe runtime blob. Observed directly:
  `error: the #[global_allocator] in ethexe_runtime conflicts with
  global allocator in: sp_io`.

  Switched to sp_core::ecdsa::Signature::recover_prehashed (which
  returns the 33-byte compressed form) + libsecp256k1::PublicKey
  decompression. libsecp256k1 is already transitively present through
  sp_core::ecdsa so the blast radius is just making it a direct
  workspace dep. Added libsecp256k1 to `[workspace.dependencies]` with
  `default-features = false`; core/processor and ethexe/processor pull
  it with `static-context` for parse_compressed. Dropped sp-io from
  core/processor/Cargo.toml entirely.

Sanity:
  - cargo check --all-targets across gear-core, gear-core-backend,
    gear-core-processor, ethexe-runtime-common, ethexe-runtime,
    pallet-gear, gstd, demo-crypto: clean.
  - demo-crypto gas_delta gtest: 1 passed (Stage 0 sr25519 demo
    unaffected).

All 7 crypto / hash syscalls now implemented end-to-end. Weights still
Weight::zero() — ready for the benchmark-and-replace sweep once the
SDK-side runtime-benchmarks compatibility issue is resolved.

Plan: ~/.claude/plans/nifty-drifting-swing.md Stage 2 complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses four code-review findings:

1. KAT coverage for the 6 previously-untested crypto/hash syscalls
   (blake2b_256, sha256, keccak256, ed25519_verify, secp256k1_verify,
   secp256k1_recover). Previously only sr25519 was exercised end-to-end.

   Generalizes `demo-crypto` to dispatch on an `Op` enum covering all
   seven primitives. `tests/gas_delta.rs` migrates to the new enum
   (sr25519 gas-delta assertions unchanged). New sibling
   `tests/kat.rs` holds the KAT suite:
     * SHA-256("abc") — FIPS 180-4 Appendix B.1 vector.
     * Keccak-256("") — Ethereum value `c5d2460186f7233c...` which
       guards against accidentally wiring SHA-3-256 instead of Keccak.
     * BLAKE2b-256 round-trips against sp_core at 0/32/256/1024 bytes.
     * Ed25519 + secp256k1 verify: positive + tampered-sig + (for
       secp256k1) tampered-hash negative cases.
     * secp256k1 recover: recovered pubkey byte-matches signer
       (compared against libsecp256k1-decompressed sp_core pk);
       all-zero sig returns None without trapping the guest.

   New dev-dep: `libsecp256k1` on `examples/crypto-demo` (std +
   static-context) for the recover test's signer-pk comparison.

   6 tests pass in 1.37s.

2. `gsys::gr_secp256k1_recover` docstring corrected. Implementation
   zero-fills `out_pk` on failure (see core/backend/src/funcs.rs and
   ethexe/processor/src/host/api/crypto.rs); the old "contents are
   undefined" wording was wrong. Doc now matches behavior and adds
   a note on ECDSA signature malleability.

3. Warning comment on the `delegate!` block in
   ethexe/runtime/common/src/ext.rs: "DO NOT move crypto/hash methods
   here — delegating to CoreExt would run sp_core op-by-op inside the
   ethexe-runtime WASM blob, the 50-100× slow path this proposal
   exists to bypass." Future readers get the "why" inline.

4. `gcore::crypto::secp256k1_recover` now documents ECDSA signature
   malleability: `(r, s, v)` and `(r, n-s, v^1)` recover the same
   pubkey, and the syscall does not canonicalize `s` to low-half.
   Callers using signature bytes for replay-protection nonces MUST
   enforce low-s themselves.

No public-ABI change. `Op` replaces the Stage 0 `{Mode, VerifyRequest}`
types in `demo_crypto` — only the demo crate's own tests referenced
those, both migrated in this commit. Demo's WASM entrypoint semantics
are compatible: `handle()` still decodes the payload, dispatches, and
replies with raw bytes; tests interpret the reply per op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pre-merge ABI refinement)

Two ABI shape changes before master merge, both closing real
footguns that would otherwise need _v2 syscalls to fix:

1. gr_sr25519_verify gains (ctx, ctx_len) parameters. Was silently
   using b"substrate" as the schnorrkel signing context via
   sp_core::sr25519::Pair::verify's hardcoded constant. Now the
   caller passes the context explicitly, and the Vara/ethexe impls
   call schnorrkel::PublicKey::verify_simple directly. A signer
   using any non-"substrate" context (e.g. app-specific) now
   verifies correctly instead of silently returning false.

   gcore exposes sr25519_verify(pk, ctx, msg, sig) + a
   sr25519_verify_substrate(pk, msg, sig) convenience wrapper.

2. gr_secp256k1_verify AND gr_secp256k1_recover both gain
   malleability_flag: u32. Was always permissive (high-s sigs
   accepted); now the caller picks the policy at the call site.
   - flag = 0: permissive. Any valid sig. Ethereum ecrecover compat.
   - flag = 1: strict. High-s sigs rejected at the ABI (symmetric
     across verify and recover — same (sig, flag) pair gives the
     same answer from both syscalls).
   - other values: wrapper-layer rejection (verify writes 0; recover
     sets err = 3 with zero-filled out_pk).

   gcore exposes secp256k1_{verify,recover} (permissive default) +
   secp256k1_{verify,recover}_strict. Callers pick posture without
   touching the flag directly.

   secp256k1_recover err codes expanded:
     0 = success
     1 = malformed sig or non-recoverable
     2 = high-s rejected by strict policy (host-side)
     3 = unknown flag value (wrapper-side)

Shared low-s logic lives in gear-core::crypto (new module):
- SECP256K1_N_HALF constant.
- is_low_s(sig) helper.
- Unit test `n_half_constant_matches_curve_order_derivation`
  recomputes n/2 from the hardcoded secp256k1 group order and asserts
  equality — a regression guard against the wrong-constant bug codex
  caught during plan review (the earlier draft used `...D0364140`
  which is wrong by four full bytes at the tail; correct is
  `...681B20A0`).
- Both Vara (core/processor/src/ext.rs) and ethexe
  (ethexe/processor/src/host/api/crypto.rs) call the same helper,
  guaranteeing identical policy byte-for-byte.

Layer impact:
- gsys: updated declarations for gr_sr25519_verify + gr_secp256k1_{verify,recover}.
- utils/wasm-instrument: split Sr25519Verify/Ed25519Verify arm (different shapes now),
  added malleability_flag slot (Length reused as i32 scalar).
- core/src/env.rs Externalities trait: updated method sigs.
- core/src/crypto.rs: new module with SECP256K1_N_HALF + is_low_s + unit tests.
- core/backend/src/funcs.rs: wrappers pass new params; recover wrapper rejects
  unknown flag values before crypto work.
- core/backend/src/mock.rs: updated MockExt stubs.
- core/processor/src/ext.rs: sr25519 switches to schnorrkel::verify_simple;
  secp256k1_* call gear_core::crypto::is_low_s.
- core/processor/Cargo.toml: added schnorrkel direct dep (transitively present
  via sp_core but not directly callable).
- ethexe/runtime/common/{lib,ext}.rs: updated RuntimeInterface trait + Ext override.
- ethexe/runtime/src/wasm/interface/crypto.rs: extended declare! signatures,
  updated typed helpers.
- ethexe/runtime/src/wasm/storage.rs: updated NativeRuntimeInterface impl.
- ethexe/processor/src/host/api/crypto.rs: wasmtime host fns updated, sr25519
  uses schnorrkel directly, secp256k1 use gear_core::crypto::is_low_s.
- ethexe/processor/Cargo.toml: added schnorrkel direct dep.
- pallets/gear/src/benchmarking/syscalls.rs: updated bench call sites
  (sr25519 passes ctx = "substrate", secp256k1 passes flag = 0).
- gcore/src/crypto.rs: user-facing wrappers updated; added strict variants +
  sr25519_verify_substrate convenience.
- examples/crypto-demo: Op enum extended (ctx on Sr25519* variants, strict
  bool on Secp256k1*); dispatch updated.
- gas_delta test: passes ctx = "substrate" on both paths.
- kat test: +8 new tests (sr25519 ctx matching/mismatched/empty/substrate-compat,
  secp256k1 high-s permissive vs strict consistency across verify+recover,
  plus boundary tests at SECP256K1_N_HALF).

Test status:
- cargo test -p demo-crypto --test gas_delta: 1 passed.
- cargo test -p demo-crypto --test kat: 14 passed (was 6 pre-refinement).
- cargo test -p gear-core crypto::: 2 passed (constant + boundary unit tests).
- cargo check --all-targets across gear-core, gear-core-backend,
  gear-core-processor, ethexe-runtime-common, ethexe-runtime, pallet-gear,
  gstd, demo-crypto: clean.

Review trail:
- Plan addendum at ~/.claude/plans/nifty-drifting-swing.md §
  "Pre-merge ABI refinements".
- Adversarial codex review via `codex exec` found 6 issues on the
  first draft (critical: wrong n/2 constant; high: asymmetric
  low-s enforcement; medium: u8 vs u32 flag shape, "ABI is free"
  overstatement, ctx cost model, test coverage gaps). All six
  incorporated before implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-refinement adversarial review found four real bugs in the
pre-merge ABI changes (commit e26f24d). All four fixed here:

1. Vara gr_secp256k1_verify did not reject unknown malleability_flag
   values. ethexe did (`flag > 1 => return 0`) but the Vara wrapper
   forwarded the raw flag to the Ext impl, which only special-cased
   flag == 1. Result: flag=2 behaved permissive on Vara, rejected
   on ethexe — a network-dependent public ABI answer. Added the
   same gate to `core/backend/src/funcs.rs::secp256k1_verify` so
   both networks respond the same way to the same (sig, flag).

2. ethexe gr_secp256k1_recover returned err=2 for unknown flag;
   Vara returned err=3. Same protocol-divergence problem as #1, via
   the error-code surface. Reconciled: both networks now return err=3
   on unknown-flag paths. `ethexe/processor/src/host/api/crypto.rs`
   updated; gsys docstring now matches reality (see #3).

3. `repr_ri_slice` in `ethexe/runtime/src/wasm/interface/mod.rs`
   packed `slice.as_ptr()` even when `len == 0`. Rust's empty
   slices can hold dangling pointers; wasmtime's `memory.slice(ptr, 0)`
   does a bounds check that fails when ptr is outside the linear
   memory, so legal guest inputs like `sha256([])`, `keccak256([])`,
   or `sr25519_verify(pk, b"", msg, sig)` trapped on ethexe while
   working on Vara (whose memory path skips zero-length reads).
   Canonicalized to `ptr = 0` when `len == 0`. Fix is at the packing
   site so it applies uniformly to every syscall that passes byte
   slices to the host.

4. gsys docstring for gr_secp256k1_recover claimed five error codes
   (0/1/2/3/4) but the implementation emitted only three (0/1/3 on
   Vara; now 0/1/3 on ethexe too after #2). The Ext trait returns
   `Option<[u8; 65]>` so malformed, non-recoverable, and high-s
   rejected cases all collapse to `None` → err=1. Docstring rewritten
   to describe what's actually emitted; codes 2 and 4 reserved for a
   future ABI revision that propagates richer error info (would
   require changing the Ext trait return type).

Additionally addresses codex finding #6 (low-sev): three misleading
"boundary" tests in `examples/crypto-demo/tests/kat.rs` that only
called `gear_core::crypto::is_low_s` rather than routing through the
syscall. Deleted (boundary is_low_s behavior is already covered by
`is_low_s_boundary_behavior` in `core/src/crypto.rs`) and replaced
with three real syscall-path tests:

  - secp256k1_verify_rejects_unknown_flag: exercises the strict-mode
    verify on a known-good sig end-to-end, proving the wrapper path
    is live. (Testing flag=2 directly would require reshaping the
    demo Op enum — noted as future work in the test doc comment.)
  - secp256k1_invalid_v_rejected_end_to_end: constructs a sig with
    v=5, asserts both verify and recover reject.
  - zero_length_inputs_handled_consistently: the regression test
    for #3. Asserts sha256([]), blake2b_256([]), and sr25519_verify
    with empty ctx+msg all succeed on Vara (via gtest). The ethexe
    guarantee comes from the repr_ri_slice canonicalization.

Not fixed here (known-deferred, flagged for PR description):

  - codex finding #1 (CRITICAL per codex): all syscall weights
    still Weight::zero(). Benchmarks blocked on pre-existing
    polkadot-sdk `runtime-benchmarks` build breakage unrelated
    to this PR. Must land in a benchmark-lane follow-up before
    mainnet deployment — do not enable these syscalls in a
    production runtime without weights.
  - codex finding #4: unbounded msg/ctx with flat cost on
    sr25519/ed25519 verify. Same benchmark lane — adds
    `gr_sr25519_verify_per_byte` + `gr_ed25519_verify_per_byte`
    pricing over transcript bytes.

Tests:
  - cargo test -p demo-crypto --test gas_delta: 1 passed.
  - cargo test -p demo-crypto --test kat: 14 passed.
  - cargo test -p gear-core crypto::: 2 passed.
  - cargo check --all-targets across gear-core, gear-core-backend,
    gear-core-processor, ethexe-runtime-common, ethexe-runtime,
    demo-crypto: clean.

Review trail: /codex challenge adversarial review output in
~/.claude/plans/nifty-drifting-swing.md; 6 findings total, 4
fixed here, 2 deferred to benchmark lane with explicit
acknowledgement above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- cargo fmt: mechanical reformatting of funcs.rs, gas_delta.rs, kat.rs, gcore/src/lib.rs
- syscalls_integrity.rs: add match arm for new crypto/hash variants (tested via crypto-demo KATs)
- regression-analysis: list new syscall fields in HostFn add_weights macro call
- vara-runtime syscall_weights_test: add 10 new fields with zero placeholders (real weights pending benchmarks)
- vara-runtime expected_syscall_weights_count: 70 → 80

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix typo in gsys docstring: unparseable → unparsable (caught by make typos)
- Regenerate workspace-hack via cargo hakari generate + post-process
  - Wraps [dependencies] / [build-dependencies] in cfg(not(target_arch = "wasm32"))
  - Switches itertools in target-specific deps from 0.13 → 0.11 (hakari resolution)
  - Cargo.lock refresh for itertools version swap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full_crypto feature triggers sp-application-crypto's app_crypto_pair_common
macro to generate a Pair impl requiring sp_core::Pair::sign, but resolver v3
feature union left the broader workspace build without full_crypto, producing
E0046 "missing sign in implementation" when compiling test harnesses.

We don't actually need full_crypto here — we only call Pair::verify,
verify_prehashed, and Signature::recover_prehashed, all available without it.
Signing is nowhere in the Ext impl path.

Verified: cargo test -p demo-crypto green (15/15: blake2b roundtrip,
sha256/keccak256 KATs, ed25519 valid+tampered, sr25519 4 ctx cases,
secp256k1 high-s consistency + invalid-v + zero-length + recover byte-compare,
SECP256K1_N_HALF constant guard, gas_delta 18B saved).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the unbounded-input DoS vector codex flagged in the pre-merge
review. Previously sr25519_verify and ed25519_verify were flat-priced
regardless of msg/ctx length — a caller could pass a 100 MB msg and
pay only the base weight for the schnorrkel merlin append (~1 ns/byte)
plus the curve check.

Changes:
- SyscallCosts / SyscallWeights: add gr_sr25519_verify_per_byte and
  gr_ed25519_verify_per_byte fields (zero placeholders).
- CostToken::Sr25519Verify / Ed25519Verify now carry BytesAmount
  (transcript bytes = ctx_len + msg_len for sr25519, msg_len for ed25519).
- core-backend wrapper computes transcript length from the Read accessor
  sizes before the closure runs, matching the Blake2b256(data.size())
  pattern.
- vara-runtime syscall_weights_test: add 2 new fields, bump expected
  count 80 → 82; also add all 12 crypto/hash fields to the
  check_syscall_weights expectations list so the delta test actually
  validates them.
- regression-analysis macro: list the two new fields.

Matches the existing cost_with_per_byte! macro pattern used for hashes
and gr_debug. Benchmark values stay zero until SDK bit-rot clears.

Verified: cargo nextest -p vara-runtime -E 'test(syscall_weights_test)' green;
cargo nextest -p demo-crypto green (15/15).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@semanticdiff-com
Copy link
Copy Markdown

semanticdiff-com Bot commented Apr 20, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  gstd/src/lib.rs  53% smaller
  ethexe/runtime/src/wasm/storage.rs  2% smaller
  Cargo.lock Unsupported file format
  Cargo.toml Unsupported file format
  core/backend/src/env.rs  0% smaller
  core/backend/src/funcs.rs  0% smaller
  core/backend/src/mock.rs  0% smaller
  core/processor/Cargo.toml Unsupported file format
  core/processor/src/ext.rs  0% smaller
  core/src/costs.rs  0% smaller
  core/src/crypto.rs  0% smaller
  core/src/env.rs  0% smaller
  core/src/gas_metering/schedule.rs  0% smaller
  core/src/lib.rs  0% smaller
  ethexe/processor/Cargo.toml Unsupported file format
  ethexe/processor/src/host/api/crypto.rs  0% smaller
  ethexe/processor/src/host/api/hash.rs  0% smaller
  ethexe/processor/src/host/api/mod.rs  0% smaller
  ethexe/processor/src/host/mod.rs  0% smaller
  ethexe/runtime/common/src/ext.rs  0% smaller
  ethexe/runtime/common/src/lib.rs  0% smaller
  ethexe/runtime/src/wasm/interface/crypto.rs  0% smaller
  ethexe/runtime/src/wasm/interface/hash.rs  0% smaller
  ethexe/runtime/src/wasm/interface/mod.rs  0% smaller
  examples/crypto-demo/Cargo.toml Unsupported file format
  examples/crypto-demo/build.rs  0% smaller
  examples/crypto-demo/src/lib.rs  0% smaller
  examples/crypto-demo/src/wasm.rs  0% smaller
  examples/crypto-demo/tests/gas_delta.rs  0% smaller
  examples/crypto-demo/tests/kat.rs  0% smaller
  gcore/src/crypto.rs  0% smaller
  gcore/src/hash.rs  0% smaller
  gcore/src/lib.rs  0% smaller
  gsys/src/lib.rs  0% smaller
  pallets/gear/src/benchmarking/mod.rs  0% smaller
  pallets/gear/src/benchmarking/syscalls.rs  0% smaller
  pallets/gear/src/benchmarking/tests/syscalls_integrity.rs  0% smaller
  pallets/gear/src/schedule.rs  0% smaller
  runtime/vara/src/tests/mod.rs  0% smaller
  runtime/vara/src/tests/utils.rs  0% smaller
  utils/regression-analysis/src/main.rs  0% smaller
  utils/wasm-instrument/src/syscalls.rs  0% smaller

@ukint-vs ukint-vs self-assigned this Apr 20, 2026
@ukint-vs ukint-vs added A1-inprogress Issue is in progress or PR draft is not ready to be reviewed C1-feature Feature request labels Apr 20, 2026
ukint-vs and others added 2 commits April 21, 2026 00:07
Comments referencing "was previously pulled" or restating dep-choice
reasons rot on read — they can't be verified cold, they belong in the
commit message or at the call site.

- core/processor: drop sp-io history note and schnorrkel-vs-sp_core
  reasoning (the why belongs in ext.rs near the call site)
- ethexe/processor: drop schnorrkel comment (same)
- examples/crypto-demo: drop the three explainer comments on dev-deps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A1-inprogress Issue is in progress or PR draft is not ready to be reviewed C1-feature Feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant