From bb74841df9b5e4756bb8d73443c97bf4b146d55e Mon Sep 17 00:00:00 2001 From: Anton Velichko <631906+tonyvelichko@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:09:00 +0400 Subject: [PATCH 1/2] build: publish zk-encryption on crates.io The wallet crate takes the workspace version, carries crates.io metadata and is published by a new crates-io release job (skipped when the version is already there). The circuits stay on the CDN. --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++++++ rust/Cargo.lock | 2 +- rust/zk-encryption/Cargo.toml | 9 +++++++-- rust/zk-encryption/README.md | 10 +++++----- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09f101f..2075d2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ # # noir ──────┐ # rust ──────┼─> ci ──> tag (cog, push:main) ──> github-release ──> publish (release assets, R2) +# └──> crates-io (zk-encryption) # typos ─────┤ # supply-chain # @@ -275,6 +276,36 @@ jobs: name: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} generate_release_notes: true + # The wallet crate on crates.io, at the released tag. A version already + # published (a re-release of an existing tag) is skipped: crates.io refuses + # a second upload of a version anyway. + crates-io: + name: Publish zk-encryption to crates.io + runs-on: ubuntu-latest + needs: [resolve, tag, github-release] + if: always() && needs.github-release.result == 'success' + env: + TAG: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rust-src-dir: rust + - name: cargo publish + working-directory: rust + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + set -euo pipefail + version="${TAG#v}" + if curl -fsS -A "zk-encryption-ci" "https://crates.io/api/v1/crates/zk-encryption/$version" >/dev/null 2>&1; then + echo "zk-encryption $version is already on crates.io" + exit 0 + fi + cargo publish -p zk-encryption --locked + # The release assets: the frozen manifest, the frozen resources (ABIs and # keys) as a tarball with its SHA-256, and catalog.json, uploaded to the # GitHub release and to the circuits bucket under diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ffff1c5..5f5767a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1985,7 +1985,7 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zk-encryption" -version = "0.1.0" +version = "0.1.3" dependencies = [ "ark-bn254", "ark-ec", diff --git a/rust/zk-encryption/Cargo.toml b/rust/zk-encryption/Cargo.toml index cfb3494..1bbc0d4 100644 --- a/rust/zk-encryption/Cargo.toml +++ b/rust/zk-encryption/Cargo.toml @@ -1,12 +1,17 @@ [package] name = "zk-encryption" -version = "0.1.0" +version.workspace = true description = "Post-quantum encrypted channel for ZK pipelines, the wallet side: the receiver's keys, bundle and scan, the sender's handshake and ratchet, payload sealing and opening, the lattice encryption and the Poseidon2 duplex the circuits match" edition.workspace = true rust-version.workspace = true license.workspace = true -publish.workspace = true +# The one crate of this workspace on crates.io (the circuits ship as release assets on the CDN). +publish = true repository.workspace = true +readme = "README.md" +documentation = "https://docs.rs/zk-encryption" +keywords = ["zero-knowledge", "post-quantum", "ml-kem", "poseidon2", "noir"] +categories = ["cryptography"] [dependencies] ark-bn254 = "0.6" diff --git a/rust/zk-encryption/README.md b/rust/zk-encryption/README.md index 7992187..31b75d7 100644 --- a/rust/zk-encryption/README.md +++ b/rust/zk-encryption/README.md @@ -1,4 +1,4 @@ -# pq-channel +# zk-encryption The post-quantum envelope channel of the folded eid + Emit V2 transfer, as a Rust library: everything a wallet or a receiver service does off-chain, matching the circuits in [`noir/`](../../noir). The protocol is [`PROTOCOL.md`](../../PROTOCOL.md); this crate is its reference implementation, and [`rust/reference`](../reference) drives it through real folded proofs against a mock chain. @@ -19,12 +19,12 @@ No proving here: the crate has no dependency on the Noir toolchain or barretenbe | `emit` | Emit V2's hashes (`Emit`), `NoteOpening`, `Kem` (the session app's outputs with the carried ciphertext), the `Envelope` event with its 2,080-byte layout, `Delivery`, `Dg1` (its encoding as a 6-field payload, `to_payload`/`from_payload`, and its sealing); `NoteOpening` is a payload too. | | `witness` | `NoirToml`: the session app's inputs (`context` and the `channel` tables) and the envelope apps' inputs (the payload, its commitment salt, `context`, `s`), in the `Prover.toml` layout the circuits take. | -`Error` is the crate's one error type (bundle parsing has its own, `bundle::BundleError`). Field elements are `ark_bn254::Fr`, re-exported as `pq_channel::Fr`. +`Error` is the crate's one error type (bundle parsing has its own, `bundle::BundleError`). Field elements are `ark_bn254::Fr`, re-exported as `zk_encryption::Fr`. ## Receiver flow ```rust -use pq_channel::{bundle::{Bundle, Ek}, emit::{Delivery, Emit, Envelope}, receiver::{Receiver, Scan}}; +use zk_encryption::{bundle::{Bundle, Ek}, emit::{Delivery, Emit, Envelope}, receiver::{Receiver, Scan}}; // Once: long-term keys and the bundle to publish. `pk` is the shielded // address (Emit::pk(sk)) transfers to this receiver commit their note to. @@ -53,7 +53,7 @@ match receiver.scan(&delivery) { ## Sender flow ```rust -use pq_channel::{sender::Sender, witness::NoirToml}; +use zk_encryption::{sender::Sender, witness::NoirToml}; // Once per receiver: accept the bundle (format, chain, validity, V, ek and // its commitment). `fetch` resolves an ek URL, or `&|_| None` for inline only. @@ -85,4 +85,4 @@ Nothing here is constant-time beyond what the underlying crates provide; see PRO ## Tests -`cargo test -p pq-channel --release` (add `--features serde` for the persistence round trip): known answers against nargo, decryption of real ML-KEM-768 ciphertexts, the extreme-noise bound, the ratchet's windows, the bundle, and a receiver's scans over natively built transfers. The folded proofs are exercised by `rust/reference`'s e2e test. +`cargo test -p zk-encryption --release` (add `--features serde` for the persistence round trip): known answers against nargo, decryption of real ML-KEM-768 ciphertexts, the extreme-noise bound, the ratchet's windows, the bundle, and a receiver's scans over natively built transfers. The folded proofs are exercised by `rust/reference`'s e2e test. From 104272c005e83fc1c37ba6f318b87c8a63897d78 Mon Sep 17 00:00:00 2001 From: Anton Velichko <631906+tonyvelichko@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:10:38 +0400 Subject: [PATCH 2/2] ci: publish the crate and the assets before the GitHub release crates-io and publish (R2, checked against what the CDN serves) run first and in parallel; github-release runs last, only when both succeeded, attaching the assets (handed over as a workflow artifact) with the downloads and the crate in its notes. --- .github/workflows/ci.yml | 100 ++++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2075d2c..6b1020b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,11 @@ # CI + release for zk-encryption (the shape of eid-circuits' ci.yml). # # noir ──────┐ -# rust ──────┼─> ci ──> tag (cog, push:main) ──> github-release ──> publish (release assets, R2) -# └──> crates-io (zk-encryption) +# rust ──────┼─> ci ──> tag (cog, push:main) ──┬─> publish (release assets, R2) ──┬─> github-release +# └─> crates-io (zk-encryption) ─────┘ +# +# Everything is published before the GitHub release exists: a release only +# appears once its assets answer on the CDN and its crate is on crates.io. # typos ─────┤ # supply-chain # @@ -261,29 +264,17 @@ jobs: echo "tag=${tag}" echo "tag=${tag}" >> "$GITHUB_OUTPUT" - github-release: - name: Create GitHub release - runs-on: ubuntu-latest - needs: [resolve, tag] - if: | - always() && - ((needs.tag.result == 'success' && needs.tag.outputs.tag != '') || - needs.resolve.outputs.release_tag != '') - steps: - - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} - name: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} - generate_release_notes: true - # The wallet crate on crates.io, at the released tag. A version already # published (a re-release of an existing tag) is skipped: crates.io refuses # a second upload of a version anyway. crates-io: name: Publish zk-encryption to crates.io runs-on: ubuntu-latest - needs: [resolve, tag, github-release] - if: always() && needs.github-release.result == 'success' + needs: [resolve, tag] + if: | + always() && + ((needs.tag.result == 'success' && needs.tag.outputs.tag != '') || + needs.resolve.outputs.release_tag != '') env: TAG: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} steps: @@ -308,18 +299,20 @@ jobs: # The release assets: the frozen manifest, the frozen resources (ABIs and # keys) as a tarball with its SHA-256, and catalog.json, uploaded to the - # GitHub release and to the circuits bucket under - # https://circuits.zk-experiments.dev/// (and - # /latest/catalog.json). The prefix is a workflow variable so - # other repositories share the bucket under their own prefixes. + # circuits bucket under https://circuits.zk-experiments.dev/// + # (and /latest/catalog.json) and handed to github-release as a + # workflow artifact. The prefix is a workflow variable so other repositories + # share the bucket under their own prefixes. publish: name: Publish release assets runs-on: ubuntu-latest - needs: [resolve, tag, github-release] - if: always() && needs.github-release.result == 'success' + needs: [resolve, tag] + if: | + always() && + ((needs.tag.result == 'success' && needs.tag.outputs.tag != '') || + needs.resolve.outputs.release_tag != '') timeout-minutes: 60 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} R2_PREFIX: ${{ vars.R2_PREFIX_ZK_ENCRYPTION || 'zk-encryption' }} R2_BASE: https://circuits.zk-experiments.dev @@ -346,12 +339,6 @@ jobs: mise run freeze:check - name: Write the release assets run: mise run release:assets - - name: Upload to the release - run: | - set -euo pipefail - for f in target/release/manifest.toml target/release/resources.tar.gz target/release/resources.tar.gz.sha256 target/release/catalog.json; do - gh release upload "$TAG" "$f" --clobber - done - name: Upload to the circuits R2 bucket if: vars.R2_CIRCUITS_ZK_EXPERIMENTS_BUCKET != '' env: @@ -373,16 +360,55 @@ jobs: curl -fsS "$base/resources.tar.gz" -o published.tar.gz echo "$(cat target/release/resources.tar.gz.sha256) published.tar.gz" | shasum -a 256 -c curl -fsS "$R2_BASE/$R2_PREFIX/latest/catalog.json" | cmp - target/release/catalog.json - - name: Add the downloads to the release notes + - name: Hand the assets to github-release + uses: actions/upload-artifact@v4 + with: + name: release-assets + path: | + target/release/manifest.toml + target/release/resources.tar.gz + target/release/resources.tar.gz.sha256 + target/release/catalog.json + if-no-files-found: error + + # Last: the release appears only once its assets are on the CDN and its crate + # is on crates.io, with the assets attached and the downloads in its notes. + github-release: + name: Create GitHub release + runs-on: ubuntu-latest + needs: [resolve, tag, crates-io, publish] + if: always() && needs.crates-io.result == 'success' && needs.publish.result == 'success' + env: + TAG: ${{ needs.tag.outputs.tag || needs.resolve.outputs.release_tag }} + R2_PREFIX: ${{ vars.R2_PREFIX_ZK_ENCRYPTION || 'zk-encryption' }} + R2_BASE: https://circuits.zk-experiments.dev + steps: + - uses: actions/download-artifact@v4 + with: + name: release-assets + path: assets + - name: The downloads section of the release notes run: | set -euo pipefail version="${TAG#v}" base="$R2_BASE/$R2_PREFIX/$version" { - gh release view "$TAG" --json body -q .body - echo echo "## Frozen circuits" echo - echo "[\`catalog.json\`]($base/catalog.json) (the families and their roots), [\`manifest.toml\`]($base/manifest.toml), [\`resources.tar.gz\`]($base/resources.tar.gz) (ABIs and verification keys, SHA-256 \`$(cat target/release/resources.tar.gz.sha256)\`); the same files are attached to this release. The \`zk-encryption-circuits\` crate carries the bytecode and every pin, so the host is a mirror, not a trust anchor." + echo "[\`catalog.json\`]($base/catalog.json) (the families and their roots), [\`manifest.toml\`]($base/manifest.toml), [\`resources.tar.gz\`]($base/resources.tar.gz) (ABIs and verification keys, SHA-256 \`$(cat assets/resources.tar.gz.sha256)\`); the same files are attached to this release. The \`zk-encryption-circuits\` crate carries the bytecode and every pin, so the host is a mirror, not a trust anchor." + echo + echo "## Crate" + echo + echo "[\`zk-encryption $version\`](https://crates.io/crates/zk-encryption/$version) on crates.io." } > notes.md - gh release edit "$TAG" --notes-file notes.md + - uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG }} + name: ${{ env.TAG }} + generate_release_notes: true + body_path: notes.md + files: | + assets/manifest.toml + assets/resources.tar.gz + assets/resources.tar.gz.sha256 + assets/catalog.json