libid_contracts/circuits.rs
1//! The ceremony circuits' UltraHonk verifiers: which circuit each platform
2//! proves under, and a deploy that links the libraries a bb verifier needs.
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 it is a contract like any other: `forge build`
11//! compiles it and `scripts/vendor-artifacts.sh` embeds it, so a consumer
12//! deploys it from [`Artifacts::embedded`] with no `bb`.
13//!
14//! bb emits `RelationsLib` and `ZKTranscriptLib` as external libraries, so a
15//! verifier's creation code carries a placeholder per call site until each
16//! library is deployed and its address linked in. bb writes a copy of both
17//! into every verifier it generates, and forge links a verifier only
18//! against the copies of its own file; the copies compile to the same
19//! bytecode, and [`Libraries`] deploys each distinct bytecode once, at an
20//! address derived from it, so every verifier links against the one
21//! deployment. [`deploy_honk_verifiers`] does that for a set of circuits
22//! and [`deploy_honk_verifier`] for one; either returns the address a
23//! [`platform_verifier::Initializer`](crate::platform_verifier::Initializer)
24//! pins — by address and by the code hash it reads off the chain, which
25//! covers the library addresses linked into the verifier's code.
26
27use std::collections::BTreeMap;
28
29use alloy::{
30 primitives::Address,
31 providers::Provider,
32};
33
34use crate::{
35 artifacts::Artifacts,
36 deploy::{
37 deploy_contract_from,
38 Libraries,
39 },
40 error::{
41 Error,
42 Result,
43 },
44};
45
46/// One ceremony circuit, and so one vendored Honk verifier.
47///
48/// Two, not three: `oidc-google` proves the Google ID Token, and
49/// `bearer-link` ties a token exchange to an identity for X and GitHub
50/// alike, because their statements are the same.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub enum Circuit {
53 /// The token-exchange circuit, shared by the `x/v1` and `github/v1`
54 /// profiles.
55 BearerLink,
56 /// The Google OIDC circuit, for `google/v1`.
57 OidcGoogle,
58}
59
60impl Circuit {
61 /// Every circuit the launch platforms verify under.
62 pub const ALL: [Self; 2] = [Self::BearerLink, Self::OidcGoogle];
63
64 /// The circuit's directory in the `libid-circuits` release, which is
65 /// also its tarball's name and its key in `circuits.json`.
66 pub const fn name(self) -> &'static str {
67 match self {
68 Self::BearerLink => "bearer-link",
69 Self::OidcGoogle => "oidc-google",
70 }
71 }
72
73 /// The vendored contract, which is also its `.sol` file and its entry
74 /// in [`COVERED`](crate::artifacts::COVERED). bb names every verifier
75 /// `HonkVerifier`; `libid-circuits` renames each so two can share a
76 /// project and one artifact path names one circuit.
77 pub const fn contract(self) -> &'static str {
78 match self {
79 Self::BearerLink => "BearerLinkHonkVerifier",
80 Self::OidcGoogle => "OidcGoogleHonkVerifier",
81 }
82 }
83}
84
85/// The external libraries every bb verifier links, vendored beside it under
86/// its own `.sol` file (the shape `linkReferences` names them in). Which
87/// verifiers share a deployment of one is decided by its bytecode at deploy
88/// time, not here.
89pub const LIBRARIES: [&str; 2] = ["RelationsLib", "ZKTranscriptLib"];
90
91/// The `libid-circuits` release the vendored verifiers came from, read from
92/// the pin `scripts/vendor-artifacts.sh` copies in as `circuits.json`.
93///
94/// A Honk verifier IS its verification key, so a consumer that names a
95/// deployment after its artifact — a CREATE3 name, say — wants this in the
96/// name: a new circuits release is a different contract and must land at a
97/// different address rather than silently replace the old one.
98///
99/// Errors for an [`Artifacts::from_dir`] over a raw forge `out/`, which
100/// carries no pin.
101pub fn version(artifacts: &Artifacts) -> Result<String> {
102 let pin: serde_json::Value = artifacts.read_json("circuits.json")?;
103 pin["version"]
104 .as_str()
105 .map(str::to_owned)
106 .ok_or_else(|| Error::Artifact {
107 detail: "circuits.json has no version".into(),
108 })
109}
110
111/// What [`deploy_honk_verifiers`] put on the chain.
112#[derive(Clone, Debug, Default, PartialEq, Eq)]
113pub struct HonkVerifiers {
114 /// Each circuit's verifier, its libraries linked.
115 pub verifiers: BTreeMap<Circuit, Address>,
116 /// The libraries they link: one deployment per distinct bytecode, and
117 /// which address each verifier's file links against.
118 pub libraries: Libraries,
119}
120
121/// Deploy the Honk verifiers of `circuits`, sharing every library whose
122/// bytecode they have in common, and return their addresses.
123///
124/// The libraries first, through [`Libraries::deploy`]: one deployment per
125/// distinct creation code, however many circuits carry a copy, and none for
126/// a bytecode already at its address. Then each verifier, its placeholders
127/// substituted with the addresses its own file resolves to; deploying the
128/// placeholder would produce a contract that reverts on every proof. For
129/// the two launch circuits that is two libraries and two verifiers — four
130/// transactions on a bare chain, not six.
131///
132/// A library is shared by bytecode alone. Circuits whose libraries differ
133/// get separate deployments, and no circuit is ever linked against bytes
134/// it was not compiled against.
135///
136/// Each address is what a Platform Verifier initializer takes as
137/// `honk_verifier`; the code hash it pins beside it is read off the chain by
138/// [`Initializer::call`](crate::platform_verifier::Initializer::call), and
139/// carries the library addresses linked in.
140///
141/// `sender` opts into explicit nonce management (see
142/// [`deploy_contract_from`]).
143pub async fn deploy_honk_verifiers<P: Provider>(
144 provider: &P,
145 artifacts: &Artifacts,
146 circuits: &[Circuit],
147 sender: Option<Address>,
148) -> Result<HonkVerifiers> {
149 let contracts: Vec<(&str, &str)> = circuits
150 .iter()
151 .map(|circuit| (circuit.contract(), circuit.contract()))
152 .collect();
153 let libraries = Libraries::deploy(provider, artifacts, &contracts, sender).await?;
154 let mut verifiers = BTreeMap::new();
155 for circuit in circuits {
156 if verifiers.contains_key(circuit) {
157 continue;
158 }
159 let contract = circuit.contract();
160 let bytecode = libraries.link(artifacts, contract, contract)?;
161 let address = deploy_contract_from(
162 provider,
163 bytecode,
164 &format!("{contract} ({} circuit)", circuit.name()),
165 sender,
166 )
167 .await?;
168 verifiers.insert(*circuit, address);
169 }
170 Ok(HonkVerifiers {
171 verifiers,
172 libraries,
173 })
174}
175
176/// Deploy `circuit`'s Honk verifier with its libraries linked, and return
177/// its address: [`deploy_honk_verifiers`] over the one circuit.
178///
179/// Called alone it still shares: a library's address is a function of its
180/// bytecode, so a copy another circuit's deploy already put on the chain is
181/// found there and linked, not deployed again. Three transactions on a bare
182/// chain, one when both libraries are in place.
183///
184/// `sender` opts into explicit nonce management (see
185/// [`deploy_contract_from`]).
186pub async fn deploy_honk_verifier<P: Provider>(
187 provider: &P,
188 artifacts: &Artifacts,
189 circuit: Circuit,
190 sender: Option<Address>,
191) -> Result<Address> {
192 let honk = deploy_honk_verifiers(provider, artifacts, &[circuit], sender).await?;
193 honk.verifiers
194 .get(&circuit)
195 .copied()
196 .ok_or_else(|| Error::Rpc {
197 detail: format!("{} verifier deploy recorded no address", circuit.name()),
198 })
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::artifacts::COVERED;
205
206 /// Every verifier and every library it links is one the crate vendors,
207 /// under the verifier's own file — a library missing from the artifacts
208 /// could only deploy with its placeholder left in.
209 #[test]
210 fn every_verifier_and_its_libraries_are_covered() {
211 let artifacts = Artifacts::embedded();
212 for circuit in Circuit::ALL {
213 let contract = circuit.contract();
214 assert!(
215 COVERED.contains(&(contract, contract)),
216 "{contract} is not in COVERED"
217 );
218 for library in LIBRARIES {
219 assert!(
220 COVERED.contains(&(contract, library)),
221 "{contract}.sol:{library} is not in COVERED"
222 );
223 }
224
225 // The artifact links exactly LIBRARIES, and nothing under another
226 // file: a verifier that linked a library the list does not name
227 // would deploy with a placeholder left in.
228 let refs = artifacts.link_references(contract, contract).unwrap();
229 let mut linked: Vec<(String, String)> = refs
230 .iter()
231 .flat_map(|(path, libs)| {
232 let stem = std::path::Path::new(path)
233 .file_stem()
234 .and_then(|s| s.to_str())
235 .unwrap_or_else(|| panic!("{contract}: bad library path {path}"))
236 .to_owned();
237 libs.as_object()
238 .into_iter()
239 .flatten()
240 .map(move |(name, _)| (stem.clone(), name.clone()))
241 })
242 .collect();
243 linked.sort();
244 let mut expected: Vec<(String, String)> = LIBRARIES
245 .iter()
246 .map(|lib| (contract.to_owned(), (*lib).to_owned()))
247 .collect();
248 expected.sort();
249 assert_eq!(linked, expected, "{contract} links something else");
250 }
251 }
252
253 /// The two circuits are distinct artifacts: a shared one would wire
254 /// both platforms to one verification key.
255 #[test]
256 fn the_two_circuits_are_different_artifacts() {
257 let artifacts = Artifacts::embedded();
258 let [bearer, oidc] = Circuit::ALL;
259 assert_ne!(bearer.contract(), oidc.contract());
260 assert_ne!(
261 artifacts
262 .bytecode_hex(bearer.contract(), bearer.contract())
263 .unwrap(),
264 artifacts
265 .bytecode_hex(oidc.contract(), oidc.contract())
266 .unwrap()
267 );
268 }
269
270 /// The enum and the pin name the same circuits under the same
271 /// contracts; the pin is what the vendor script follows, the enum what
272 /// a consumer deploys by.
273 #[test]
274 fn the_enum_matches_the_pin() {
275 let artifacts = Artifacts::embedded();
276 let pin: serde_json::Value = artifacts.read_json("circuits.json").unwrap();
277 let circuits = pin["circuits"].as_object().expect("circuits object");
278 assert_eq!(circuits.len(), Circuit::ALL.len());
279 for circuit in Circuit::ALL {
280 let entry = &circuits[circuit.name()];
281 assert_eq!(entry["contract"].as_str(), Some(circuit.contract()));
282 let digest = entry["sha256"].as_str().expect("sha256 string");
283 assert_eq!(digest.len(), 64, "{}: not a sha256", circuit.name());
284 }
285 let version = version(&artifacts).unwrap();
286 assert!(
287 version.split('.').count() == 3,
288 "circuits.json version '{version}' is not major.minor.patch"
289 );
290 }
291}