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}