feat(crypto): native crypto + hash syscalls on Vara and ethexe#5363
Draft
feat(crypto): native crypto + hash syscalls on Vara and ethexe#5363
Conversation
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds 7
gr_*syscalls with byte-identical ABI on Vara and ethexe.Syscalls
gr_blake2b_256gr_sha256gr_keccak256gr_sr25519_verifygr_ed25519_verifygr_secp256k1_verifymalleability_flag: u32(0 permissive / 1 strict low-s)gr_secp256k1_recoverArchitecture
One shared wrapper in
core/backend/funcs. Both networks enter through it,and it charges gas once upstream via
CostToken/SyscallWeights.From there:
Externalitiesimpl incore/processor/extthatruns
sp_core::{sr25519,ed25519,ecdsa},libsecp256k1, andschnorrkeldirectly in the runtime.
Externalitiesmethods inethexe/runtime/common/extto call newext_*_v1host imports, whichethexe/processorservices viawasmtime::Linker::func_wrapusing thesame crypto crates — natively, not inside the WASM runtime blob.
Ethexe crypto methods are intentionally not routed through
delegate!.Delegating would run
sp_corecompiled 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 singlegear_core::cryptomodule soverifyandrecovercan never disagree onthe same
(sig, flag).Verified
demo-cryptoKATs (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_HALFconstantdemo-cryptogas_delta test exists and passesmake fmt,make clippy-gear,make typoscleanvara-runtime syscall_weights_testgreen (82-field count)Deferred
Weight::zero()placeholders. Real benchmarks blocked onpre-existing polkadot-sdk
runtime-benchmarksfeature bit-rot (reproduceson master).
make test+make clippy-examplesfail with pre-existingpolkadot-sdk / macOS issues (reproduced on master).