Skip to content

Security/zk core hardening#107

Merged
nol4lej merged 10 commits into
mainfrom
security/zk-core-hardening
Jul 2, 2026
Merged

Security/zk core hardening#107
nol4lej merged 10 commits into
mainfrom
security/zk-core-hardening

Conversation

@nol4lej

@nol4lej nol4lej commented Jul 2, 2026

Copy link
Copy Markdown
Member

orbinum-zk-core 1.1.0

Security and robustness hardening of the Poseidon primitives:

  • No panics in the hashing path or host functions (consensus-safe)
  • Non-zero fallback that cannot collide with the dummy sentinel
  • Canonical little-endian field encoding checks (is_canonical_le / from_canonical_le)
  • circomlib known-answer and native/WASM equivalence test vectors
  • no_std feature-matrix documented and CI-guarded

See primitives/zk-core/CHANGELOG.md for details.

nol4lej added 10 commits July 2, 2026 11:42
The Poseidon hashers used `.expect()` on both circuit init and hashing.
A panic there behaves differently across execution environments: a native
panic aborts the node process while a WASM panic is a recoverable trap.
The same malformed input could therefore crash native validators while
WASM nodes merely reject the block — a consensus-divergence risk.

Route all arities (hash_2, hash_4, hash_5, poseidon_hash_1) through a
single internal `poseidon_circom` helper that returns a deterministic
value instead of panicking. The failure path is provably unreachable
(constant arity, already-reduced Fr inputs) and is now documented as an
invariant, keeping native and WASM behavior identical by construction.

Add tests covering boundary field elements (0 and p-1) for no-panic and
determinism, plus core/trait equivalence.
poseidon_hash_2/4 asserted their inputs were exactly 32 bytes and then
copy_from_slice'd them. Both paths panic on a wrong-length input. As
native host functions a panic aborts the node process (a WASM trap is
recoverable), so a malformed length could crash native validators while
WASM nodes stay up — a consensus-divergence risk.

Read inputs through a read_32_le helper that zero-pads or truncates to
32 bytes deterministically and never panics. Callers always pass 32
bytes, so this is purely defensive; behavior is identical on native and
WASM.
… vectors

The existing native-vs-WASM test compared the host function against
LightPoseidonHasher, which the host calls internally — a tautology. And
nothing checked the hash against the circuit it must match.

Add tests/poseidon_vectors.rs with canonical circomlib BN254 Poseidon
outputs for arities 1/2/4/5, plus native/WASM equivalence over a spread
of inputs. These vectors fail if light-poseidon-nostd ever drifts from
the compiled circuit's parameters. Document the Native==Wasm==Circuit
invariant in the crate docs.
Fr::from_le_bytes_mod_order reduces its input mod p, so x and x+p (both
32 bytes) yield the same field element while being different byte
strings. Layers that compare raw bytes — nullifier sets, commitment
maps, external-address commitments — would treat them as distinct while
a proof over the reduced element accepts both, a collision/double-spend
vector.

Add FieldElement::is_canonical_le and from_canonical_le so trust
boundaries can reject non-canonical (>= p) encodings before use. The
host functions are unchanged: they only receive bytes produced by
field_to_bytes, which are canonical by construction. Applying the check
at the raw-byte boundaries lives in the consuming pallets.
The crate is no_std but `default` enables std. Consumers that forget
`default-features = false` would leak std into the WASM runtime build.
All in-tree consumers already disable defaults; this locks that in.

Document the contract in Cargo.toml and add a CI step that checks the
crate builds with --no-default-features and with only poseidon-native,
so a future feature or dependency change can't silently break the WASM
build. `default` is left unchanged (Substrate convention; no consumer
relies on it).
field_to_bytes / bytes_to_field and the host-function output used
copy_from_slice(&bytes[..32]), which panics if to_bytes_le() ever
returns fewer than 32 bytes. BN254 Fr always yields 32, so this was
latent, but the slice is fragile under a type change.

Copy by min-length (zero-pad/truncate) and reuse read_32_le for the
host output. Add round-trip and no-panic tests for boundary field
elements.
@nol4lej nol4lej merged commit 9309595 into main Jul 2, 2026
6 checks passed
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