Skip to content

fix(contracts): reject non-BLS12-381 verifying keys in circom-to-soroban-hex - #306

Open
tim777z wants to merge 4 commits into
vouchlabsio:mainfrom
tim777z:bounty/65-bounty---85--make-circom-to-soroban-hex
Open

tim777z wants to merge 4 commits into
vouchlabsio:mainfrom
tim777z:bounty/65-bounty---85--make-circom-to-soroban-hex

Conversation

@tim777z

@tim777z tim777z commented Nov 9, 2026 •

Copy link
Copy Markdown

Closes #65

Problem

circom-to-soroban-hex never read the curve field of the input JSON. Running it on the committed BN254 key (packages/circuits/build/verification_key.json) silently re-encodes it into the BLS12-381 byte layout. The resulting blob contains points that are not on BLS12-381 and only fails much later, inside VerificationKey::from_bytes.

Fix

packages/contracts/circom-to-soroban-hex/src/main.rs

  • Read curve and refuse anything that is not BLS12-381. Names are normalised first, so bls12381 (what circom writes), bls12-381 (issue title) and BLS12_381 are all accepted — a naive != "bls12-381" check rejects the real committed key.
  • Validate every group element before encoding: field-range parse, z == 1, and is_on_curve. A mis-curved or hand-edited key cannot slip through even if the curve field is spoofed.
  • vk_hex now returns Result<_, VkError>; main prints the reason to stderr and exits 1 with empty stdout, so a shell pipeline can never capture a mis-curved blob.
  • Cargo.toml: remove the duplicated ark-bls12-381 / ark-serialize keys (the manifest declared both 0.4.0 and 0.6.0, so the crate did not resolve).

Tests

cargo test --manifest-path packages/contracts/circom-to-soroban-hex/Cargo.toml → 6 passed, 0 failed

  • bn128_key_is_rejected_without_hex — the committed BN254 key returns CurveMismatch("bn128") and no hex.
  • bls12381_key_emits_expected_layout — the committed BLS12-381 key encodes to 1060 bytes (2120 hex chars), IC length prefix 00000004.
  • real_vk_g2_points_round_trip — the encoded beta_2 decodes back through G2Affine::deserialize_uncompressed, the same path G2Affine::from_array uses on-chain.
  • g2_limbs_serialize_as_c1_then_c0 — pins arkworks' real limb order (ark-bls12-381 0.4.0, curves/g2.rs::serialize_with_mode). The previous test asserted the opposite order and could never pass; that is why it was removed rather than fixed.
  • curve_spellings_are_equivalent, missing_curve_is_rejected.

Verification (local, Rust 1.99.0)

$ cargo run -q --manifest-path packages/contracts/circom-to-soroban-hex/Cargo.toml \
    -- vk packages/circuits/build/verification_key.json
error: curve mismatch: expected bls12-381 (circom writes "bls12381"), got "bn128"; refusing to re-encode a key from another curve
# exit 1, 0 bytes on stdout

$ cargo run -q ... -- vk packages/zk/build/verification_key.json
# exit 0, 2120 hex chars (1060 bytes), IC length prefix 00000004

cargo fmt --check and cargo clippy --all-targets -- -D warnings are clean.

The CI check reports action_required because this PR comes from a fork and needs maintainer approval before workflows run — it is not a test failure.

tim777z and others added 4 commits November 9, 2026 21:50
…ban-hex

`circom-to-soroban-hex` never read the `curve` field of the input JSON, so a
BN254 key was silently re-encoded into the BLS12-381 byte layout. The resulting
blob contains points that are not on BLS12-381 and only fails later, deep inside
`VerificationKey::from_bytes`.

Changes:
- read `curve` and refuse anything but BLS12-381, normalising `bls12381`,
  `bls12-381` and `BLS12_381` so the committed circom key is accepted (the
  previous naive `!= "bls12-381"` check rejected the real key);
- validate every group element before encoding: field-range parse, `z == 1`,
  and `is_on_curve`, so a mis-curved or hand-edited key cannot slip through;
- return `Result<_, VkError>` from `vk_hex`; `main` prints the reason to stderr
  and exits 1 with empty stdout, so a shell pipeline can never capture a
  mis-curved key blob;
- restore the G2 limb-order test with arkworks' actual order (`c1` then `c0`,
  `ark-bls12-381 0.4.0` `curves/g2.rs::serialize_with_mode`) and add a
  round-trip test that decodes the committed key's `beta_2` back through
  `G2Affine::deserialize_uncompressed`, the same path `G2Affine::from_array`
  uses on-chain.

Acceptance evidence (local, Rust 1.99.0):
- bn128 key  -> exit 1, 0 bytes on stdout,
  stderr: curve mismatch: ... got "bn128"; refusing to re-encode ...
- bls12381 key -> 1060 bytes (2120 hex), IC length prefix `00000004`
- `cargo test --manifest-path packages/contracts/circom-to-soroban-hex/Cargo.toml`
  -> 6 passed, 0 failed
- `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` clean
@tim777z tim777z changed the title fix: [Bounty: $85] Make circom-to-soroban-hex parse a BLS12-381 ver... fix(contracts): reject non-BLS12-381 verifying keys in circom-to-soroban-hex Nov 10, 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.

[Bounty: $85] Make circom-to-soroban-hex parse a BLS12-381 verifying key

1 participant