Skip to main content

libid_contracts/
circuits.rs

1//! The ceremony circuits' UltraHonk verifiers: which circuit each platform
2//! proves under, and the deploy of a circuit's verifier.
3//!
4//! A Honk verifier is not written in this repository. bb derives it from a
5//! circuit's verification key, `libid-circuits` runs bb and ships the
6//! Solidity in its release tarballs, and `scripts/vendor-circuit-verifiers.sh`
7//! downloads the pinned release — checked against digests committed in
8//! `solidity/contracts/circuits/circuits.json` — formats it and writes it
9//! under `solidity/contracts/circuits`, gitignored and vendored again before
10//! every build. From there `forge build` compiles it, on the legacy
11//! pipeline `solidity/foundry.toml` sets for these files, and
12//! `scripts/vendor-artifacts.sh` embeds it, so a consumer deploys it from
13//! [`Artifacts::embedded`] with no `bb`.
14//!
15//! The verifier is bb's optimized template: one contract, deployed in one
16//! transaction by [`deploy_honk_verifier`]. Its address is what a
17//! [`platform_verifier::Initializer`](crate::platform_verifier::Initializer)
18//! pins, beside the code hash it reads off the chain.
19
20use alloy::{
21    primitives::Address,
22    providers::Provider,
23};
24
25use crate::{
26    artifacts::Artifacts,
27    deploy::deploy_contract_from,
28    error::{
29        Error,
30        Result,
31    },
32};
33
34/// One ceremony circuit, and so one vendored Honk verifier.
35///
36/// Two, not three: `oidc-google` proves the Google ID Token, and
37/// `bearer-link` ties a token exchange to an identity for X and GitHub
38/// alike, because their statements are the same.
39#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum Circuit {
41    /// The token-exchange circuit, shared by the `x/v1` and `github/v1`
42    /// profiles.
43    BearerLink,
44    /// The Google OIDC circuit, for `google/v1`.
45    OidcGoogle,
46}
47
48impl Circuit {
49    /// Every circuit the launch platforms verify under.
50    pub const ALL: [Self; 2] = [Self::BearerLink, Self::OidcGoogle];
51
52    /// The circuit's directory in the `libid-circuits` release, which is
53    /// also its tarball's name and its key in `circuits.json`.
54    pub const fn name(self) -> &'static str {
55        match self {
56            Self::BearerLink => "bearer-link",
57            Self::OidcGoogle => "oidc-google",
58        }
59    }
60
61    /// The vendored contract, which is also its `.sol` file and its entry
62    /// in [`COVERED`](crate::artifacts::COVERED). bb names every verifier
63    /// `HonkVerifier`; `libid-circuits` renames each so two can share a
64    /// project and one artifact path names one circuit.
65    pub const fn contract(self) -> &'static str {
66        match self {
67            Self::BearerLink => "BearerLinkHonkVerifier",
68            Self::OidcGoogle => "OidcGoogleHonkVerifier",
69        }
70    }
71}
72
73/// The `libid-circuits` release the vendored verifiers came from, read from
74/// the pin `scripts/vendor-artifacts.sh` copies in as `circuits.json`.
75///
76/// A Honk verifier IS its verification key, so a consumer that names a
77/// deployment after its artifact — a CREATE3 name, say — wants this in the
78/// name: a new circuits release is a different contract and must land at a
79/// different address rather than silently replace the old one.
80///
81/// Errors for an [`Artifacts::from_dir`] over a raw forge `out/`, which
82/// carries no pin.
83pub fn version(artifacts: &Artifacts) -> Result<String> {
84    let pin: serde_json::Value = artifacts.read_json("circuits.json")?;
85    pin["version"]
86        .as_str()
87        .map(str::to_owned)
88        .ok_or_else(|| Error::Artifact {
89            detail: "circuits.json has no version".into(),
90        })
91}
92
93/// Deploy `circuit`'s Honk verifier and return its address, which a
94/// Platform Verifier initializer takes as `honk_verifier`.
95///
96/// `sender` opts into explicit nonce management (see
97/// [`deploy_contract_from`]).
98pub async fn deploy_honk_verifier<P: Provider>(
99    provider: &P,
100    artifacts: &Artifacts,
101    circuit: Circuit,
102    sender: Option<Address>,
103) -> Result<Address> {
104    deploy_contract_from(
105        provider,
106        artifacts.bytecode(circuit.contract())?,
107        &format!("{} ({} circuit)", circuit.contract(), circuit.name()),
108        sender,
109    )
110    .await
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::artifacts::COVERED;
117
118    /// Every circuit's verifier is one the crate vendors.
119    #[test]
120    fn every_verifier_is_covered() {
121        for circuit in Circuit::ALL {
122            let contract = circuit.contract();
123            assert!(
124                COVERED.contains(&(contract, contract)),
125                "{contract} is not in COVERED"
126            );
127        }
128    }
129
130    /// The two circuits are distinct artifacts: a shared one would wire
131    /// both platforms to one verification key.
132    #[test]
133    fn the_two_circuits_are_different_artifacts() {
134        let artifacts = Artifacts::embedded();
135        let [bearer, oidc] = Circuit::ALL;
136        assert_ne!(bearer.contract(), oidc.contract());
137        assert_ne!(
138            artifacts
139                .bytecode_hex(bearer.contract(), bearer.contract())
140                .unwrap(),
141            artifacts
142                .bytecode_hex(oidc.contract(), oidc.contract())
143                .unwrap()
144        );
145    }
146
147    /// The enum and the pin name the same circuits under the same
148    /// contracts; the pin is what the vendor script follows, the enum what
149    /// a consumer deploys by.
150    #[test]
151    fn the_enum_matches_the_pin() {
152        let artifacts = Artifacts::embedded();
153        let pin: serde_json::Value = artifacts.read_json("circuits.json").unwrap();
154        let circuits = pin["circuits"].as_object().expect("circuits object");
155        assert_eq!(circuits.len(), Circuit::ALL.len());
156        for circuit in Circuit::ALL {
157            let entry = &circuits[circuit.name()];
158            assert_eq!(entry["contract"].as_str(), Some(circuit.contract()));
159            let digest = entry["sha256"].as_str().expect("sha256 string");
160            assert_eq!(digest.len(), 64, "{}: not a sha256", circuit.name());
161        }
162        let version = version(&artifacts).unwrap();
163        assert!(
164            version.split('.').count() == 3,
165            "circuits.json version '{version}' is not major.minor.patch"
166        );
167    }
168}