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. [`deploy_honk_verifier`]
17//! does all of that and returns the verifier's address, which is what a
18//! [`platform_verifier::Initializer`](crate::platform_verifier::Initializer)
19//! pins — by address and by the code hash it reads off the chain.
20
21use alloy::{
22 primitives::Address,
23 providers::Provider,
24};
25
26use crate::{
27 artifacts::Artifacts,
28 deploy::deploy_contract_from,
29 error::{
30 Error,
31 Result,
32 },
33};
34
35/// One ceremony circuit, and so one vendored Honk verifier.
36///
37/// Two, not three: `oidc-google` proves the Google ID Token, and
38/// `bearer-link` ties a token exchange to an identity for X and GitHub
39/// alike, because their statements are the same.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub enum Circuit {
42 /// The token-exchange circuit, shared by the `x/v1` and `github/v1`
43 /// profiles.
44 BearerLink,
45 /// The Google OIDC circuit, for `google/v1`.
46 OidcGoogle,
47}
48
49impl Circuit {
50 /// Every circuit the launch platforms verify under.
51 pub const ALL: [Self; 2] = [Self::BearerLink, Self::OidcGoogle];
52
53 /// The circuit's directory in the `libid-circuits` release, which is
54 /// also its tarball's name and its key in `circuits.json`.
55 pub const fn name(self) -> &'static str {
56 match self {
57 Self::BearerLink => "bearer-link",
58 Self::OidcGoogle => "oidc-google",
59 }
60 }
61
62 /// The vendored contract, which is also its `.sol` file and its entry
63 /// in [`COVERED`](crate::artifacts::COVERED). bb names every verifier
64 /// `HonkVerifier`; `libid-circuits` renames each so two can share a
65 /// project and one artifact path names one circuit.
66 pub const fn contract(self) -> &'static str {
67 match self {
68 Self::BearerLink => "BearerLinkHonkVerifier",
69 Self::OidcGoogle => "OidcGoogleHonkVerifier",
70 }
71 }
72}
73
74/// The external libraries every bb verifier links, vendored beside it under
75/// its own `.sol` file (the shape `linkReferences` names them in).
76pub const LIBRARIES: [&str; 2] = ["RelationsLib", "ZKTranscriptLib"];
77
78/// The `libid-circuits` release the vendored verifiers came from, read from
79/// the pin `scripts/vendor-artifacts.sh` copies in as `circuits.json`.
80///
81/// A Honk verifier IS its verification key, so a consumer that names a
82/// deployment after its artifact — a CREATE3 name, say — wants this in the
83/// name: a new circuits release is a different contract and must land at a
84/// different address rather than silently replace the old one.
85///
86/// Errors for an [`Artifacts::from_dir`] over a raw forge `out/`, which
87/// carries no pin.
88pub fn version(artifacts: &Artifacts) -> Result<String> {
89 let pin: serde_json::Value = artifacts.read_json("circuits.json")?;
90 pin["version"]
91 .as_str()
92 .map(str::to_owned)
93 .ok_or_else(|| Error::Artifact {
94 detail: "circuits.json has no version".into(),
95 })
96}
97
98/// Deploy `circuit`'s Honk verifier with its libraries linked, and return
99/// its address.
100///
101/// Each library is deployed first and its address substituted into the
102/// verifier's creation code, then the verifier itself; deploying the
103/// placeholder would produce a contract that reverts on every proof. Three
104/// transactions per circuit. The libraries are not shared between the two
105/// circuits: each verifier's file carries its own copy, and forge links a
106/// verifier only against the libraries of its own file.
107///
108/// The address is what a Platform Verifier initializer takes as
109/// `honk_verifier`; the code hash it pins beside it is read off the chain by
110/// [`Initializer::call`](crate::platform_verifier::Initializer::call).
111///
112/// `sender` opts into explicit nonce management (see
113/// [`deploy_contract_from`]).
114pub async fn deploy_honk_verifier<P: Provider>(
115 provider: &P,
116 artifacts: &Artifacts,
117 circuit: Circuit,
118 sender: Option<Address>,
119) -> Result<Address> {
120 let contract = circuit.contract();
121 let bytecode = artifacts
122 .linked_bytecode(provider, contract, contract, sender)
123 .await?;
124 deploy_contract_from(
125 provider,
126 bytecode,
127 &format!("{contract} ({} circuit)", circuit.name()),
128 sender,
129 )
130 .await
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::artifacts::COVERED;
137
138 /// Every verifier and every library it links is one the crate vendors,
139 /// under the verifier's own file — a library missing from the artifacts
140 /// could only deploy with its placeholder left in.
141 #[test]
142 fn every_verifier_and_its_libraries_are_covered() {
143 let artifacts = Artifacts::embedded();
144 for circuit in Circuit::ALL {
145 let contract = circuit.contract();
146 assert!(
147 COVERED.contains(&(contract, contract)),
148 "{contract} is not in COVERED"
149 );
150 for library in LIBRARIES {
151 assert!(
152 COVERED.contains(&(contract, library)),
153 "{contract}.sol:{library} is not in COVERED"
154 );
155 }
156
157 // The artifact links exactly LIBRARIES, and nothing under another
158 // file: a verifier that linked a library the list does not name
159 // would deploy with a placeholder left in.
160 let refs = artifacts.link_references(contract, contract).unwrap();
161 let mut linked: Vec<(String, String)> = refs
162 .iter()
163 .flat_map(|(path, libs)| {
164 let stem = std::path::Path::new(path)
165 .file_stem()
166 .and_then(|s| s.to_str())
167 .unwrap_or_else(|| panic!("{contract}: bad library path {path}"))
168 .to_owned();
169 libs.as_object()
170 .into_iter()
171 .flatten()
172 .map(move |(name, _)| (stem.clone(), name.clone()))
173 })
174 .collect();
175 linked.sort();
176 let mut expected: Vec<(String, String)> = LIBRARIES
177 .iter()
178 .map(|lib| (contract.to_owned(), (*lib).to_owned()))
179 .collect();
180 expected.sort();
181 assert_eq!(linked, expected, "{contract} links something else");
182 }
183 }
184
185 /// The two circuits are distinct artifacts: a shared one would wire
186 /// both platforms to one verification key.
187 #[test]
188 fn the_two_circuits_are_different_artifacts() {
189 let artifacts = Artifacts::embedded();
190 let [bearer, oidc] = Circuit::ALL;
191 assert_ne!(bearer.contract(), oidc.contract());
192 assert_ne!(
193 artifacts
194 .bytecode_hex(bearer.contract(), bearer.contract())
195 .unwrap(),
196 artifacts
197 .bytecode_hex(oidc.contract(), oidc.contract())
198 .unwrap()
199 );
200 }
201
202 /// The enum and the pin name the same circuits under the same
203 /// contracts; the pin is what the vendor script follows, the enum what
204 /// a consumer deploys by.
205 #[test]
206 fn the_enum_matches_the_pin() {
207 let artifacts = Artifacts::embedded();
208 let pin: serde_json::Value = artifacts.read_json("circuits.json").unwrap();
209 let circuits = pin["circuits"].as_object().expect("circuits object");
210 assert_eq!(circuits.len(), Circuit::ALL.len());
211 for circuit in Circuit::ALL {
212 let entry = &circuits[circuit.name()];
213 assert_eq!(entry["contract"].as_str(), Some(circuit.contract()));
214 let digest = entry["sha256"].as_str().expect("sha256 string");
215 assert_eq!(digest.len(), 64, "{}: not a sha256", circuit.name());
216 }
217 let version = version(&artifacts).unwrap();
218 assert!(
219 version.split('.').count() == 3,
220 "circuits.json version '{version}' is not major.minor.patch"
221 );
222 }
223}