From 539968f158e0d779f584de2821090f715a3b25e1 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:03:30 +0200 Subject: [PATCH] feat(confidential): wire UltraHonk backend into verifier Back ConfidentialVerifier::verify_proof with the UltraHonk verifier from NethermindEth/rs-soroban-ultrahonk (pinned commit). Add bb-generated packed binary VKs alongside the JSON fields, plus an example verifier contract. --- Cargo.lock | 19 ++++ Cargo.toml | 4 + examples/confidential/verifier/Cargo.toml | 24 +++++ .../confidential/verifier/src/contract.rs | 61 ++++++++++++ examples/confidential/verifier/src/lib.rs | 5 + examples/confidential/verifier/src/test.rs | 93 +++++++++++++++++ packages/tokens/Cargo.toml | 5 +- .../circuits/scripts/build_vk_bins.sh | 94 ++++++++++++++++++ .../src/confidential/circuits/vks/README.md | 37 +++++-- .../confidential/circuits/vks/register.vk.bin | Bin 0 -> 1760 bytes .../circuits/vks/revoke_spender.vk.bin | Bin 0 -> 1760 bytes .../circuits/vks/set_spender.vk.bin | Bin 0 -> 1760 bytes .../circuits/vks/spender_transfer.vk.bin | Bin 0 -> 1760 bytes .../confidential/circuits/vks/transfer.vk.bin | Bin 0 -> 1760 bytes .../confidential/circuits/vks/withdraw.vk.bin | Bin 0 -> 1760 bytes .../tokens/src/confidential/verifier/mod.rs | 65 ++++++++---- .../src/confidential/verifier/storage.rs | 37 +++++++ .../tokens/src/confidential/verifier/test.rs | 29 +++++- 18 files changed, 440 insertions(+), 33 deletions(-) create mode 100644 examples/confidential/verifier/Cargo.toml create mode 100644 examples/confidential/verifier/src/contract.rs create mode 100644 examples/confidential/verifier/src/lib.rs create mode 100644 examples/confidential/verifier/src/test.rs create mode 100755 packages/tokens/src/confidential/circuits/scripts/build_vk_bins.sh create mode 100644 packages/tokens/src/confidential/circuits/vks/register.vk.bin create mode 100644 packages/tokens/src/confidential/circuits/vks/revoke_spender.vk.bin create mode 100644 packages/tokens/src/confidential/circuits/vks/set_spender.vk.bin create mode 100644 packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.bin create mode 100644 packages/tokens/src/confidential/circuits/vks/transfer.vk.bin create mode 100644 packages/tokens/src/confidential/circuits/vks/withdraw.vk.bin diff --git a/Cargo.lock b/Cargo.lock index 3fb764f77..f607b36ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,6 +495,16 @@ dependencies = [ "stellar-tokens", ] +[[package]] +name = "confidential-verifier-example" +version = "0.7.1" +dependencies = [ + "soroban-sdk", + "stellar-access", + "stellar-macros", + "stellar-tokens", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -2452,6 +2462,7 @@ dependencies = [ "stellar-contract-utils", "stellar-event-assertion", "stellar-governance", + "ultrahonk_soroban_verifier", ] [[package]] @@ -2598,6 +2609,14 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "ultrahonk_soroban_verifier" +version = "0.1.0" +source = "git+https://github.com/NethermindEth/rs-soroban-ultrahonk?rev=661db07200f890b1bd9a7349ed787c70a706dd12#661db07200f890b1bd9a7349ed787c70a706dd12" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "unarray" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index 3dea2d7e9..6633d518c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,10 @@ p256 = "0.13.2" serde = { version = "1", default-features = false } serde-json-core = { version = "0.6.0", default-features = false } +# UltraHonk proof-verification backend for the confidential token. Pinned to a +# specific commit because the upstream crate is pre-release and unaudited. +ultrahonk-soroban-verifier = { git = "https://github.com/NethermindEth/rs-soroban-ultrahonk", rev = "661db07200f890b1bd9a7349ed787c70a706dd12", package = "ultrahonk_soroban_verifier", default-features = false } + # members stellar-access = { path = "packages/access", version = "0.7.1" } stellar-accounts = { path = "packages/accounts", version = "0.7.1" } diff --git a/examples/confidential/verifier/Cargo.toml b/examples/confidential/verifier/Cargo.toml new file mode 100644 index 000000000..f583b88c7 --- /dev/null +++ b/examples/confidential/verifier/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "confidential-verifier-example" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +version.workspace = true +authors.workspace = true + +[package.metadata.stellar] +cargo_inherit = true + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } +stellar-access = { workspace = true } +stellar-macros = { workspace = true } +stellar-tokens = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } diff --git a/examples/confidential/verifier/src/contract.rs b/examples/confidential/verifier/src/contract.rs new file mode 100644 index 000000000..89cbc7c66 --- /dev/null +++ b/examples/confidential/verifier/src/contract.rs @@ -0,0 +1,61 @@ +//! Confidential Verifier Example Contract. +//! +//! A deployable [`ConfidentialVerifier`] registry: it stores one UltraHonk +//! verification key per [`CircuitType`] and exposes `verify_proof` to the +//! confidential token (called cross-contract on every state-changing +//! operation). VK management is gated behind a `manager` role; `verify_proof` +//! and `get_verification_key` use the trait's default implementations, which +//! run the UltraHonk backend from `NethermindEth/rs-soroban-ultrahonk`. +//! +//! # ⚠️ Not Production Ready +//! +//! The UltraHonk backend and the circuits the verification keys are derived +//! from are **not audited**. Do not deploy this anywhere handling real value. +//! +//! # Security +//! +//! `update_verification_key` is a soundness-critical break-glass operation: a +//! wrong key makes the circuit accept forged proofs. This example gates it +//! behind the same `manager` role as registration purely for illustration. A +//! real deployment should follow the trait's guidance — ship VKs immutably +//! where possible, and put any update path behind multisig + timelock. +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, Env, Symbol, Vec}; +use stellar_access::access_control::{self as access_control, AccessControl}; +use stellar_macros::only_role; +use stellar_tokens::confidential::verifier::{ + storage as verifier, CircuitType, ConfidentialVerifier, +}; + +const MANAGER_ROLE: Symbol = symbol_short!("manager"); + +#[contract] +pub struct ConfidentialVerifierContract; + +#[contractimpl] +impl ConfidentialVerifierContract { + pub fn __constructor(e: &Env, admin: Address, manager: Address) { + access_control::set_admin(e, &admin); + access_control::grant_role_no_auth(e, &manager, &MANAGER_ROLE, &admin); + } +} + +#[contractimpl(contracttrait)] +impl ConfidentialVerifier for ConfidentialVerifierContract { + #[only_role(operator, "manager")] + fn register_verification_key(e: &Env, circuit_type: CircuitType, vk: Bytes, operator: Address) { + verifier::register_verification_key(e, circuit_type, &vk); + } + + #[only_role(operator, "manager")] + fn update_verification_key( + e: &Env, + circuit_type: CircuitType, + new_vk: Bytes, + operator: Address, + ) { + verifier::update_verification_key(e, circuit_type, &new_vk); + } +} + +#[contractimpl(contracttrait)] +impl AccessControl for ConfidentialVerifierContract {} diff --git a/examples/confidential/verifier/src/lib.rs b/examples/confidential/verifier/src/lib.rs new file mode 100644 index 000000000..a879b6f80 --- /dev/null +++ b/examples/confidential/verifier/src/lib.rs @@ -0,0 +1,5 @@ +#![no_std] + +pub mod contract; +#[cfg(test)] +mod test; diff --git a/examples/confidential/verifier/src/test.rs b/examples/confidential/verifier/src/test.rs new file mode 100644 index 000000000..cc844d676 --- /dev/null +++ b/examples/confidential/verifier/src/test.rs @@ -0,0 +1,93 @@ +extern crate std; + +use soroban_sdk::{testutils::Address as _, Address, Bytes, Env}; +use stellar_tokens::confidential::verifier::CircuitType; + +use crate::contract::{ConfidentialVerifierContract, ConfidentialVerifierContractClient}; + +// Real UltraHonk verification keys in the packed on-chain format, generated +// from the committed circuits by `circuits/scripts/build_vk_bins.sh`. Using +// real keys here exercises the wired UltraHonk backend end to end: a malformed +// key would be rejected by `UltraHonkVerifier::new` with `#3403`. +const REGISTER_VK: &[u8; 1760] = + include_bytes!("../../../../packages/tokens/src/confidential/circuits/vks/register.vk.bin"); +const WITHDRAW_VK: &[u8; 1760] = + include_bytes!("../../../../packages/tokens/src/confidential/circuits/vks/withdraw.vk.bin"); + +fn create_client<'a>( + e: &Env, + admin: &Address, + manager: &Address, +) -> ConfidentialVerifierContractClient<'a> { + let address = e.register(ConfidentialVerifierContract, (admin, manager)); + ConfidentialVerifierContractClient::new(e, &address) +} + +#[test] +fn register_and_get_verification_key_works() { + let e = Env::default(); + e.mock_all_auths(); + let admin = Address::generate(&e); + let manager = Address::generate(&e); + let client = create_client(&e, &admin, &manager); + + let vk = Bytes::from_array(&e, REGISTER_VK); + client.register_verification_key(&CircuitType::Register, &vk, &manager); + + assert_eq!(client.get_verification_key(&CircuitType::Register), vk); +} + +#[test] +fn verify_proof_runs_backend_on_real_vk() { + let e = Env::default(); + e.mock_all_auths(); + let admin = Address::generate(&e); + let manager = Address::generate(&e); + let client = create_client(&e, &admin, &manager); + + client.register_verification_key( + &CircuitType::Register, + &Bytes::from_array(&e, REGISTER_VK), + &manager, + ); + + // The key parses (no `#3403`), so the UltraHonk backend actually runs and + // rejects a junk proof rather than panicking. A real positive case needs a + // matching proof + public inputs produced by the prover toolchain. + let junk = Bytes::from_array(&e, &[0u8; 32]); + assert!(!client.verify_proof(&CircuitType::Register, &junk, &junk)); +} + +#[test] +fn update_verification_key_replaces_in_place() { + let e = Env::default(); + e.mock_all_auths(); + let admin = Address::generate(&e); + let manager = Address::generate(&e); + let client = create_client(&e, &admin, &manager); + + let old = Bytes::from_array(&e, REGISTER_VK); + let new = Bytes::from_array(&e, WITHDRAW_VK); + + client.register_verification_key(&CircuitType::Register, &old, &manager); + client.update_verification_key(&CircuitType::Register, &new, &manager); + + assert_eq!(client.get_verification_key(&CircuitType::Register), new); +} + +#[test] +#[should_panic(expected = "Error(Contract, #2000)")] +fn register_by_non_manager_panics() { + let e = Env::default(); + e.mock_all_auths(); + let admin = Address::generate(&e); + let manager = Address::generate(&e); + let stranger = Address::generate(&e); + let client = create_client(&e, &admin, &manager); + + client.register_verification_key( + &CircuitType::Register, + &Bytes::from_array(&e, REGISTER_VK), + &stranger, + ); +} diff --git a/packages/tokens/Cargo.toml b/packages/tokens/Cargo.toml index 37e61159d..36f023573 100644 --- a/packages/tokens/Cargo.toml +++ b/packages/tokens/Cargo.toml @@ -14,11 +14,14 @@ cargo_inherit = true crate-type = ["lib", "cdylib"] doctest = false +# The `alloc` feature provides the global allocator that the UltraHonk verifier +# backend (`ultrahonk-soroban-verifier`) requires in `no_std` wasm builds. [dependencies] -soroban-sdk = { workspace = true } +soroban-sdk = { workspace = true, features = ["alloc"] } soroban-poseidon = { workspace = true } stellar-contract-utils = { workspace = true } stellar-governance = { workspace = true } +ultrahonk-soroban-verifier = { workspace = true } [dev-dependencies] ed25519-dalek = { workspace = true } diff --git a/packages/tokens/src/confidential/circuits/scripts/build_vk_bins.sh b/packages/tokens/src/confidential/circuits/scripts/build_vk_bins.sh new file mode 100755 index 000000000..98ddb2888 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/scripts/build_vk_bins.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Builds the packed binary verification keys consumed on-chain by the +# UltraHonk verifier (`ultrahonk-soroban-verifier`) and writes them under +# `vks/.vk.bin`, one file per per-operation circuit, alongside the +# human-readable `vks/.vk.json` produced by `extract_vks.sh`. +# +# Why a separate binary artifact: +# - `vks/*.vk.json` is bb's `fields` output (a JSON array of hex `Fr` +# elements). It is committed for cross-platform-stable code review and is +# the format diffed by CI, but it is NOT the byte layout the verifier +# parses. +# - `ultrahonk-soroban-verifier::load_vk_from_bytes` expects exactly 1760 +# bytes: a 32-byte header of four big-endian u64s +# (circuit_size, log_circuit_size, public_inputs_size, pub_inputs_offset) +# followed by 27 G1 commitments at 64 bytes each (x || y, big-endian). +# +# `bb write_vk` (default `bytes` output) emits this exact layout with ONE +# extra 4-byte field appended to the header: a big-endian u32 holding the +# number of user public inputs (= public_inputs_size - PAIRING_POINTS_SIZE), +# which the verifier recomputes and does not store. The bb file is therefore +# 1764 bytes laid out as: +# +# [0..32) four big-endian u64 header words (kept) +# [32..36) big-endian u32 user-PI count (dropped) +# [36..1764) 27 * 64-byte G1 commitments (kept) +# +# This script strips bytes [32..36) to obtain the 1760-byte file. The VK bytes +# themselves come straight from bb -- nothing here recomputes or rederives the +# key material. +# +# Requires the pinned `nargo` and `bb` versions declared in +# `.github/workflows/noir.yml`. Run from anywhere; the script anchors to the +# circuits/ root. Keep the circuit list in sync with `extract_vks.sh`. +set -euo pipefail + +cd "$(dirname "$0")/.." + +CIRCUITS=( + "register" + "withdraw" + "transfer" + "set_spender" + "spender_transfer" + "revoke_spender" +) + +OUT_DIR="vks" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +# bb's binary VK layout (see header): 32-byte header, then a 4-byte u32, then +# the commitments. The verifier omits the u32, so the on-chain file is 4 bytes +# shorter. +BB_VK_LEN=1764 +HEADER_LEN=32 +DROP_LEN=4 +PACKED_VK_LEN=1760 + +mkdir -p "$OUT_DIR" + +for name in "${CIRCUITS[@]}"; do + pkg="circuit_${name}" + bytecode="target/${pkg}.json" + + echo "==> Compiling ${pkg}" + nargo compile --package "$pkg" + + echo "==> Writing VK for ${pkg}" + stage="${TMP_DIR}/${name}" + mkdir -p "$stage" + bb write_vk -s ultra_honk -b "$bytecode" -o "$stage" + + src="${stage}/vk" + src_len="$(wc -c < "$src")" + if [ "$src_len" -ne "$BB_VK_LEN" ]; then + echo " ERROR: expected bb VK of ${BB_VK_LEN} bytes, got ${src_len}." >&2 + echo " The bb VK layout may have changed; re-validate the strip offsets." >&2 + exit 1 + fi + + out="${OUT_DIR}/${name}.vk.bin" + # Keep the four u64 header words, drop the u32 user-PI count, keep the rest. + head -c "$HEADER_LEN" "$src" > "$out" + tail -c "+$((HEADER_LEN + DROP_LEN + 1))" "$src" >> "$out" + + out_len="$(wc -c < "$out")" + if [ "$out_len" -ne "$PACKED_VK_LEN" ]; then + echo " ERROR: packed VK is ${out_len} bytes, expected ${PACKED_VK_LEN}." >&2 + exit 1 + fi + echo " wrote ${out} (${out_len} bytes)" +done + +echo "Done." diff --git a/packages/tokens/src/confidential/circuits/vks/README.md b/packages/tokens/src/confidential/circuits/vks/README.md index 5d4179b2b..3e044f30a 100644 --- a/packages/tokens/src/confidential/circuits/vks/README.md +++ b/packages/tokens/src/confidential/circuits/vks/README.md @@ -1,13 +1,25 @@ # Verification keys -UltraHonk verification keys for the per-operation circuits, one JSON file -per circuit (`.vk.json`). These are committed artifacts -- the -integration contract with the verifier (#701). - -**Format:** each file is a JSON array of hex-encoded `Fr` elements, produced -by `bb write_vk --output_format fields`. Used instead of bb's raw `bytes` -format because the latter includes platform-dependent header bytes that -spuriously break cross-platform reproducibility (macOS vs Linux CI). +UltraHonk verification keys for the per-operation circuits, two files per +circuit. These are committed artifacts -- the integration contract with the +verifier (#701). + +- **`.vk.json`** -- a JSON array of hex-encoded `Fr` elements, produced + by `bb write_vk --output_format fields`. This is the human-readable, + review-friendly form and the one CI diffs. It is *not* the byte layout the + verifier parses. Used instead of bb's raw `bytes` format because the latter + includes platform-dependent header bytes that spuriously break + cross-platform reproducibility (macOS vs Linux CI). +- **`.vk.bin`** -- the packed binary key the on-chain verifier actually + consumes (`ultrahonk-soroban-verifier::load_vk_from_bytes`): a 1760-byte + blob made of a 32-byte header (four big-endian `u64`s -- `circuit_size`, + `log_circuit_size`, `public_inputs_size`, `pub_inputs_offset`) followed by + 27 G1 commitments at 64 bytes each (`x || y`, big-endian). It is bb's + default `bytes` output with the redundant 4-byte trailing header field (the + user-public-input count, which the verifier recomputes) stripped. The key + material is bb's verbatim output -- nothing is recomputed off-chain. The + point section is byte-identical to the field elements in the matching + `.vk.json`. Reproducible from the circuit sources with the pinned toolchain: @@ -23,9 +35,16 @@ extraction and diffs against the files here; any drift fails the build. ```bash cd packages/tokens/src/confidential/circuits -./scripts/extract_vks.sh +./scripts/extract_vks.sh # regenerates the *.vk.json (CI-diffed) +./scripts/build_vk_bins.sh # regenerates the *.vk.bin (the on-chain form) ``` +Both scripts compile the circuits with the same pinned toolchain, so the +two formats stay in lockstep -- always run both when a circuit changes and +commit the result in the same PR. CI diffs the `.vk.json`; since each +`.vk.bin` point section is byte-identical to its `.vk.json`, that diff +transitively guards the key material in the binary too. + If the diff is intentional (the circuit changed), regenerate and commit in the same PR. If unintentional (e.g. toolchain bumped without an explicit decision), do **not** regenerate -- track down the source first. diff --git a/packages/tokens/src/confidential/circuits/vks/register.vk.bin b/packages/tokens/src/confidential/circuits/vks/register.vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..adf6bffa0af76777a961102c859ff254eb4758d0 GIT binary patch literal 1760 zcmZ{l`9Bkk1IO{mG3H^mxv~*ulQX`hNGM|FXslSs9CJTwQ;tlzkLQbOWpm8+xi_&Ne_Hwh2LL z*>dYs;IQt}X}0ca(73ixxpSz24#{{Qb!9#8I{_HGELrKat5-1aG**ox<3(^SQAY;o zz;?KKYWEyjfbbk!ov;{ir#8R;0J^-8xu=;zF!%*nQ)RknF!BmjDStSN^Gk$IiB(n^ z12k`+WCUkYe~dd4=?%Hmvzt5eeRMY9x~mVvvg~{ZQLAG;%RgkfEkor+O|5n6CF1et zRTYTxEZ2AtuO57UO;x@VgnAH)=5=fNU;5RNlG}W><$^e~LW|hd(M}Bni!NzH-U#@& zR#YP9+RJA#ioK$g8qOb8MF;dT0bzJf_5=Mh5;6q;lMrVS5z8=1i{i@m`YZUn?wPS| z?;-_AFb~9))>g>VDS4ZK~GVU?sJ83e}p(&-BPMlZ-A2uBp)}xGrj!$JfwpwHp+sS{FG^1)4#I z1PfuGeD+lv(yE6KPFe}N^1Y){MciTZ+SX zbs7~F37O)0Nt(t)N)Zl+0)@LTso*(A^Fdle&o%A?fw8i@3+rZ;9Fhm=l~;A`51n`B z4f{@DSN8#%fT*>m)?{`ziz&i)8;#fvVfiP6G5U5RTlPvOA?xMw{uh%{bj5e|NM{$e zItMT}Up^zluzHx{8s8g^=GqYYv@*7iX`Arui1oE|-BXQ{Ni$&h+j~;m>b4BeBy~rA zMAH@AD#e4(Eu8Uf_Jw!@tP0?o6UxEw(VZ{y)f^sNa1XkAK-C}r5C+9=(f09R5K@ZumL&W%tz zY~U05+BfG2MP*mHPobXIQC<$lie`x>Mdr)hXXWPRPo`uY6Tx4$gcTLzm~L?nUjv#< zWrA{mM~t~=zL{i)auvTgzAQvqZ$?LXoA+*w-@{u z8nlmwZ#T|k)5F7^L1X#7O}8Lhgp)P{g5(7u?O7|$XR z$K&#DHZ4&W3+l>u1T7Xsr{!_E5XGY)#~W=lstWysdSJ^_#OlEVoarm6rPXiIY{U(I zbzYjnX-=U{em`3>Dh5v+fg^gR-+aU%>&+V_56EIIfv2I)22_=~hYO#|pvxA%594S% zqd&Xq#<7~57j1Po85(LS8ot5`SPfp!>okKFRiGu=t1|bvA%V%Wq2x+XDtq5s zj8Ti@j}>Gm&pzu{N-ve{8>jIFk$%QGNlDrTo@^0TslNSV=s9%6tZbT~I@X?zuV)Tq1Dfobn-ZHgH+ds_Ol45M2?fhUac_O51}b5Sq3T@9>$6gZFt?5 zocmapQ((VPrcHm5cz3bmYP1oYXqgqSrwo*i2r^5)F8>FD8f**o8b*-HAlCzy8=hJrSjC>Ge`U*yK1`_*cI)C954Z^q~}< sYliu@%E_pQ_`wYLhVQ_w_X4R7fCRsx7yfq%Qxj_>Ucq+QqY3%{0P*H482|tP literal 0 HcmV?d00001 diff --git a/packages/tokens/src/confidential/circuits/vks/revoke_spender.vk.bin b/packages/tokens/src/confidential/circuits/vks/revoke_spender.vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..9dbef814b678ff88460801a147f65756bd280d61 GIT binary patch literal 1760 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 literal 0 HcmV?d00001 diff --git a/packages/tokens/src/confidential/circuits/vks/transfer.vk.bin b/packages/tokens/src/confidential/circuits/vks/transfer.vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..4d91725fd7f512e5d31bde4751604ef82f34e15e GIT binary patch literal 1760 zcmZ{lX*(N;0);KHg-}akQj8H>kY>hpi(1>J#)L|_jcea2T1zbz9>iJ_tzDxUT$I$V zXsCUwWr~SOwLz!U7E1*UsUVG+xzEhUIUnA0p7#$N0f9f0{D1xdnZE}7uNIW>{O`s& zu|*+5$tOxZPZR20&1G~nc5+`Dqtdq{8uh@2H5HXuy2BS>@shbMxfXh2HlGfA;w^%a zqsDC?YrvUYfAMD@-hS7ysWooG8!FlDmb_{j#;-m^kAKSEz$m98ibX3R^?_SUhiN4o zD=9`0dwTN1JqcMo&{^rTXqoDWcjW^#PJ&6(=moC{q-QfLsi*4I;hNVhZ#;Z$`1{5h z5+}n^uM_iXyRh!{D#2b(!|7)sRM>vB*P)O4D}{x*1C~v@!j%Qv9w(QLE^e=+Da9y= z<7iI;+EM1ij-|+3(&)Y^Q}nvW#kVf( zFu&8{TbK+bx!oj%Rks6&ga_LL_#sH&bpa=1HtbjVCenP~#%u*(2kC_9ttQ;;4L;%P zKd4(io|?#b4WN$a%bc+`S$!X#xveka`_z#|9M`PHT^eK(A9hiL?WW4V zHrhzvOXCuCtiU{QtFSCH1sv*m`AW1$I4a~xn20KN1Rl`U1*&Wy*dZ9w;;`9bS~5P` zXsYzM43L7MG6#xyqT@GM3bVlP*0?XFFcrGs72aL+;$s_`^qGPKw>Vk%?M({XPlCp$X`4W zZ1@(L02QCNdM*sHT?QnDjZ`W5{g{-55X(U?ArMvrNNG4V89gV&WcDbNki~i_)pZ^6 zR;HGOpP&3&qf9diH%cqv^kajpN=ZZs6*~n3wV5v|WVLqO;&a1pMRDiqryqMWwc1>e z<0i|cV&KNWS>TNWEz4_BKMXz$%JvH8XA+LTiA|Js`Km~-2chdg-C-~T>()80POCMe zf0*Xyf=fNhdFy)HSZm2k>{3TY@_YRtwn+{9tPa-_-6rx3DqmGA?x}0*Kw`y6je8&> zPj8c*@;J~?Q(#aYhe#5J26TyrFok4uX>k`6Gnm=btSxPB&`>z9?YXXxoxN1{xPhKN zQS%VD$3d_P+C-iX)J!6Ee5e9gWQ+yJHc}fZnW-9HgY*5ulS?Fdlg{JrM<%X$u>F6E z8airL^banS+Hy80s*aYoOUT@k>o{k!KrRQ>2fu{VcE$)JZDiRN1KK&^8;J#LDzJo$ zm(p6p&%0Q_)!gw#>Mt(E-zpGgUB;R$^UhNpH8u>*ja9;fJ)QCoVxJzgaI9=ZdGQuT zz2Jb~*k*k>0`kXJCxv?0&6t%|0dy;I^?mVaH1&oN|c01+a?z=%F-u+0 zBr&;=GIcrRP6gz#VJ)sHp~=#$8v+*pIcrjRRJ)rn4a=-18hNa1 zeGye-O}dshxUL7f+11TgphXork(mAcsJ*_l|YohWVH7G;13ldw9{8k{PWgs-jOBFdNngH z@i365A1ul7D>@GTH@~0S)HM^da7l2%aHT+!30ZIhpS(~*yH?^7v~Gq0zSr36<>Kuk zNk0Up11Iog?{bQhUJ4XWfrcAd=I$eo?E#(W*j4p zlsIRd0!bN;2;FC>>|dSu`&j;=1^>bSt%U#-6_Dla>#aF_hFz5ENN!xQl8=D%@Pt&T qB*O?y2+-lC#gy+iDNQ+KIeIrc0w=gSzX-g!OB1q*jv3MxEBzfow>}*J literal 0 HcmV?d00001 diff --git a/packages/tokens/src/confidential/circuits/vks/withdraw.vk.bin b/packages/tokens/src/confidential/circuits/vks/withdraw.vk.bin new file mode 100644 index 0000000000000000000000000000000000000000..073b8875182452d1e5e2a3647a42d2f9ba44df3c GIT binary patch literal 1760 zcmZ{l`9Bj30LC{gOl%nQR+~9uGiOOU%-qS9nxljq)iOu!tEJZ!VmW3n?@PI|)N&Pt zSqha&*jA1xITpFkx#B%O@B8EX{P2CA&-WjA0D!-9>mU9F$iGAJ-v&a*@KSAw>3l-9 z@QL2cg1N@??!KCbAqyZ4-6MYUR6Fni1w7+;TZMOyt@m%d7=8x+gT2v)n&4V)W4zCfx8QVHvM2-~-y zIr|vK2!yVn@hHEn#ChAB8{kig- zA2WJaT-n+H*Q82HNc0Q{QW}3SgFs-OvJIyQdWqq2r%On@aG-7RGUQ9oz@T#v6-ESSVHp$ zp|)LTry&3nndWLgMor!yMP6;0yrmg27aj&XI7T+E)Gu7A&3adxdV5{CItnI|#0vBX zny?{+XM7T-ZBBK!Lz1RUs{UiP=&Rb?2PGg_c^lv(F(> zS)eFA^KBHrWVi9Vhl}rA`fE`+!?n2JF=V$A`8Gq>@aq*O?!M5$j0^SHp%8etRtDI# zWtUKT`c|j3WM`&8bu4hW{%33xD_@|vC@Gjv-b9$B$PS#BL&j-0lH)Eq5}Vs@0#+SD z&01|VPZ>~}`A|PW0hi`$&_Q;od%CUBDee|@qjH2|@1Z+ak=1_~S>$m=F+yOHB-%y6~G7*+eW=yNfjh;luK_uDwci?uV~Re0y2Y4I|6?zXbG zoT)(f7s*_mWIcByj75yO9Kd)9+N3$U%-R4o2%nWu2<yl~xYw5k+pdm_|1>{r#WoH(^#)myWVR*s{4NpstWy#P<5Ubwc;C!PjkFW2 zG&}jEM!-Og?m1eqk*p0FYMJ(OOmPiX>@+W8-xRUP(Q${X@p@q&+4YADL2-pd+L6v# zReCG@-uo5ZVpM9W>P!Z8z^`yU9LrSQeY z=?&hP*dtYA9Y9l8jink=*7=do)N%vn2y)p11?{*nE7dtQhGqhkP4A92^vb#|egv50 zeyp6zUYX>~_vm>A*K9L=c-a#jO5ggO^dYtZm%Lpu0*#Mob-ActMg>~s1vg&)!BKQ21E zTBCF*vmka6Dh}6_4e5}?2ORg((5mWs;J!7C7Q*D|G0Ov5Tt}SHz}RQRrP=WElnB^F zMa{PpwIIimD$lgmz(z(?L~)yl)aSJuMgjSoa8lAB-?6Uj!{ft+s)J6e(T_wHvwzfr zZCh1_cWax!(}Wz3G0%3_L#A}9$*K)sQWgBe#e$G>DP>ojlQ5Iz?N5#0tBnO!N2}lM zc;bJXnzMc4jU*i|2KPY2+}YQl{Ch4UhbI*9}-{p_`-N@?rd2nzt<3`TOX)z0j3*hw8VtH`n^9fbv6i@Gjz{_ zND+kfU%bOpq+M?0^ikhIyB31QB1iSO;U)8St=tEtYSu~M%99B+wHdSvd^sNt{(f4k zQpbaM{gNKsCPH4)KDU9;a49mxSOKtt8j6GxeVs3kkk>8R%>8?_c8V*F@DJN1s|n@o9_F$ sGH+q#WbLsHB`s_^ip#bQ9bKuepZ%pO2><{9 literal 0 HcmV?d00001 diff --git a/packages/tokens/src/confidential/verifier/mod.rs b/packages/tokens/src/confidential/verifier/mod.rs index 0c4aa88bd..2cd38106b 100644 --- a/packages/tokens/src/confidential/verifier/mod.rs +++ b/packages/tokens/src/confidential/verifier/mod.rs @@ -10,15 +10,15 @@ //! //! # ⚠️ Not Production Ready //! -//! This module is **unfinished**. [`ConfidentialVerifier::verify_proof`] has no -//! working default implementation because its UltraHonk backend -//! ([`Oghma/rs-soroban-ultrahonk`](https://github.com/Oghma/rs-soroban-ultrahonk)) -//! is still under development and **has not been audited**. Do **not** deploy a -//! contract built on this trait to mainnet or any environment that handles -//! real value. The trait surface, the [`VerifierStorageKey`] layout, and the -//! VK-management helpers in [`storage`] are stable enough for the confidential -//! token to scaffold against, and they are the only part of this -//! module that is intended to be relied upon today. +//! [`ConfidentialVerifier::verify_proof`] is backed by the UltraHonk verifier +//! from +//! [`NethermindEth/rs-soroban-ultrahonk`](https://github.com/NethermindEth/rs-soroban-ultrahonk), +//! pinned to a specific commit in the workspace `Cargo.toml`. That backend is +//! **pre-release and has not been audited**, and neither have the circuits the +//! verification keys are derived from. Do **not** deploy a contract built on +//! this trait to mainnet or any environment that handles real value until both +//! the backend and the circuits have been audited and the dependency is pinned +//! to a released, reviewed version. //! //! ## Why a Separate Contract //! @@ -34,11 +34,18 @@ //! //! ## VK Encoding //! -//! Verification keys are opaque [`Bytes`] blobs from this module's point of -//! view; structural validation lives in the future UltraHonk backend, not in -//! the storage layer. The on-disk reference format committed under -//! `circuits/vks/` is a JSON array of hex-encoded `Fr` field elements (one -//! file per circuit, produced by `bb write_vk --output_format fields`). +//! Verification keys are opaque [`Bytes`] blobs from the storage layer's point +//! of view: [`register_verification_key`] and [`update_verification_key`] store +//! them verbatim without inspection. Structural validation happens lazily, in +//! the UltraHonk backend, the first time [`verify_proof`] parses the stored +//! bytes. +//! +//! The bytes registered on-chain must be the backend's packed binary VK +//! encoding (a fixed-size header followed by the curve-point commitments), not +//! the human-readable reference committed under `circuits/vks/` — that is a +//! JSON array of hex-encoded `Fr` field elements (one file per circuit, +//! produced by `bb write_vk --output_format fields`) and must be converted to +//! the packed encoding off-chain before registration. //! //! ## Storage //! @@ -87,7 +94,8 @@ mod test; use soroban_sdk::{contracterror, contractevent, contracttrait, contracttype, Address, Bytes, Env}; pub use storage::{ - get_verification_key, register_verification_key, update_verification_key, VerifierStorageKey, + get_verification_key, register_verification_key, update_verification_key, verify_proof, + VerifierStorageKey, }; /// Identifier of a zero-knowledge circuit whose verification key is stored in @@ -216,16 +224,26 @@ pub trait ConfidentialVerifier { /// /// * [`VerifierError::VerificationKeyNotRegistered`] - When `circuit_type` /// has no registered key. + /// * [`VerifierError::InvalidVerificationKey`] - When the registered key + /// cannot be parsed as a valid UltraHonk verification key. /// /// # Notes /// - /// No default implementation is provided. The UltraHonk verification - /// backend lives in `Oghma/rs-soroban-ultrahonk`, which is still - /// under development and has not been audited (see the module-level - /// warning). Implementors MUST NOT ship a stub that returns `true` - /// unconditionally to any environment that handles real value. - fn verify_proof(e: &Env, circuit_type: CircuitType, public_inputs: Bytes, proof: Bytes) - -> bool; + /// The default implementation delegates to [`storage::verify_proof`], which + /// runs the UltraHonk verifier from + /// [`NethermindEth/rs-soroban-ultrahonk`](https://github.com/NethermindEth/rs-soroban-ultrahonk). + /// That backend and the circuits the verification keys are derived from are + /// **not yet audited** (see the module-level warning); the default + /// implementation MUST NOT be relied upon in any environment that handles + /// real value until they are. + fn verify_proof( + e: &Env, + circuit_type: CircuitType, + public_inputs: Bytes, + proof: Bytes, + ) -> bool { + storage::verify_proof(e, circuit_type, &public_inputs, &proof) + } /// Returns the UltraHonk verification key registered under `circuit_type`. /// @@ -255,6 +273,9 @@ pub enum VerifierError { VerificationKeyNotRegistered = 3401, /// Indicates the proof failed UltraHonk verification. InvalidProof = 3402, + /// Indicates the registered verification key could not be parsed as a valid + /// UltraHonk verification key. + InvalidVerificationKey = 3403, } // ################## EVENTS ################## diff --git a/packages/tokens/src/confidential/verifier/storage.rs b/packages/tokens/src/confidential/verifier/storage.rs index 81dbb24a1..5648f3d92 100644 --- a/packages/tokens/src/confidential/verifier/storage.rs +++ b/packages/tokens/src/confidential/verifier/storage.rs @@ -1,4 +1,5 @@ use soroban_sdk::{contracttype, panic_with_error, Bytes, Env}; +use ultrahonk_soroban_verifier::UltraHonkVerifier; use crate::confidential::verifier::{ emit_verification_key_registered, emit_verification_key_updated, CircuitType, VerifierError, @@ -31,6 +32,42 @@ pub fn get_verification_key(e: &Env, circuit_type: CircuitType) -> Bytes { .unwrap_or_else(|| panic_with_error!(e, VerifierError::VerificationKeyNotRegistered)) } +/// Verifies an UltraHonk `proof` for `public_inputs` against the verification +/// key registered under `circuit_type`, returning `true` iff the proof is +/// valid. +/// +/// The UltraHonk backend lives in the external +/// [`ultrahonk_soroban_verifier`](https://github.com/NethermindEth/rs-soroban-ultrahonk) +/// crate. A malformed proof or mismatched public inputs is not an error here: +/// the function simply returns `false` and lets the caller decide how to react +/// (the confidential token reverts with its own `InvalidProof` error). +/// +/// # Arguments +/// +/// * `e` - Access to the Soroban environment. +/// * `circuit_type` - The circuit the proof was produced against. +/// * `public_inputs` - The serialized public inputs the prover committed to. +/// * `proof` - The serialized UltraHonk proof. +/// +/// # Errors +/// +/// * [`VerifierError::VerificationKeyNotRegistered`] - When `circuit_type` has +/// no registered key. +/// * [`VerifierError::InvalidVerificationKey`] - When the registered key cannot +/// be parsed as a valid UltraHonk verification key. +pub fn verify_proof( + e: &Env, + circuit_type: CircuitType, + public_inputs: &Bytes, + proof: &Bytes, +) -> bool { + let vk = get_verification_key(e, circuit_type); + let verifier = UltraHonkVerifier::new(e, &vk) + .unwrap_or_else(|_| panic_with_error!(e, VerifierError::InvalidVerificationKey)); + + verifier.verify(e, proof, public_inputs).is_ok() +} + // ################## CHANGE STATE ################## /// Registers an UltraHonk verification key under a fresh [`CircuitType`]. diff --git a/packages/tokens/src/confidential/verifier/test.rs b/packages/tokens/src/confidential/verifier/test.rs index 78d4352d2..ab81b8297 100644 --- a/packages/tokens/src/confidential/verifier/test.rs +++ b/packages/tokens/src/confidential/verifier/test.rs @@ -5,7 +5,7 @@ use stellar_event_assertion::EventAssertion; use crate::confidential::verifier::{ storage::{ - get_verification_key, register_verification_key, update_verification_key, + get_verification_key, register_verification_key, update_verification_key, verify_proof, VerifierStorageKey, }, CircuitType, @@ -141,3 +141,30 @@ fn storage_key_round_trip() { assert_eq!(stored, vk); }); } + +#[test] +#[should_panic(expected = "Error(Contract, #3401)")] +fn verify_proof_unregistered_panics_with_not_registered() { + let e = Env::default(); + let address = e.register(MockContract, ()); + + e.as_contract(&address, || { + let empty = Bytes::new(&e); + let _ = verify_proof(&e, CircuitType::Register, &empty, &empty); + }); +} + +#[test] +#[should_panic(expected = "Error(Contract, #3403)")] +fn verify_proof_malformed_vk_panics_with_invalid_vk() { + let e = Env::default(); + let address = e.register(MockContract, ()); + + e.as_contract(&address, || { + // A 32-byte blob is not a valid UltraHonk verification key, so the + // backend rejects it before any proof is examined. + register_verification_key(&e, CircuitType::Transfer, &vk_bytes(&e, 0xab)); + let empty = Bytes::new(&e); + let _ = verify_proof(&e, CircuitType::Transfer, &empty, &empty); + }); +}