1use alloy::{
25 primitives::{
26 keccak256,
27 Address,
28 B256,
29 },
30 providers::Provider,
31 sol_types::SolCall,
32};
33
34use crate::{
35 artifacts::Artifacts,
36 bindings::ceremony::{
37 GooglePlatformVerifier,
38 TlsNotaryPlatformVerifier,
39 },
40 circuits::Circuit,
41 deploy::deploy_behind_proxy,
42 error::{
43 Error,
44 Result,
45 },
46};
47
48pub const MAX_PROOF_LIFETIME: u64 = 30 * 24 * 60 * 60;
50pub const MAX_FUTURE_ATTESTATION_SKEW: u64 = 24 * 60 * 60;
53pub const MAX_FUTURE_OBSERVATION_ALLOWANCE: u64 = 24 * 60 * 60;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub enum PlatformVerifier {
60 X,
62 GitHub,
64 Google,
67}
68
69impl PlatformVerifier {
70 pub const ALL: [Self; 3] = [Self::X, Self::GitHub, Self::Google];
72
73 pub const fn contract(self) -> &'static str {
76 match self {
77 Self::X => "XPlatformVerifier",
78 Self::GitHub => "GitHubPlatformVerifier",
79 Self::Google => "GooglePlatformVerifier",
80 }
81 }
82
83 pub const fn platform(self) -> &'static str {
86 match self {
87 Self::X => "x",
88 Self::GitHub => "github",
89 Self::Google => "google",
90 }
91 }
92
93 pub fn platform_id(self) -> B256 {
96 keccak256(self.platform().as_bytes())
97 }
98
99 pub const fn circuit(self) -> Circuit {
102 match self {
103 Self::X | Self::GitHub => Circuit::BearerLink,
104 Self::Google => Circuit::OidcGoogle,
105 }
106 }
107
108 pub const fn notarizes(self) -> bool {
114 match self {
115 Self::X | Self::GitHub => true,
116 Self::Google => false,
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub struct TlsNotaryRoots {
125 pub owner: Address,
127 pub notary_service: Address,
130 pub honk_verifier: Address,
133 pub proof_lifetime: u64,
136 pub max_future_attestation_skew: u64,
139 pub future_observation_allowance: u64,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct GoogleRoots {
149 pub owner: Address,
151 pub honk_verifier: Address,
153 pub future_observation_allowance: u64,
157 pub jwt_roots: Address,
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum Initializer {
164 X(TlsNotaryRoots),
165 GitHub(TlsNotaryRoots),
166 Google(GoogleRoots),
167}
168
169#[derive(Clone, Debug, PartialEq, Eq)]
174pub enum InitializeCall {
175 TlsNotary(TlsNotaryPlatformVerifier::initializeCall),
178 Google(GooglePlatformVerifier::initializeCall),
180}
181
182impl InitializeCall {
183 pub fn abi_encode(&self) -> Vec<u8> {
185 match self {
186 Self::TlsNotary(call) => call.abi_encode(),
187 Self::Google(call) => call.abi_encode(),
188 }
189 }
190
191 pub fn honk_verifier_codehash(&self) -> B256 {
193 match self {
194 Self::TlsNotary(call) => call.honkVerifierCodehash_,
195 Self::Google(call) => call.honkVerifierCodehash_,
196 }
197 }
198}
199
200impl Initializer {
201 pub const fn verifier(&self) -> PlatformVerifier {
203 match self {
204 Self::X(_) => PlatformVerifier::X,
205 Self::GitHub(_) => PlatformVerifier::GitHub,
206 Self::Google(_) => PlatformVerifier::Google,
207 }
208 }
209
210 pub const fn honk_verifier(&self) -> Address {
212 match self {
213 Self::X(roots) | Self::GitHub(roots) => roots.honk_verifier,
214 Self::Google(roots) => roots.honk_verifier,
215 }
216 }
217
218 pub fn check(&self) -> Result<()> {
224 let contract = self.verifier().contract();
225 let refuse = |detail: String| Error::Initializer {
226 detail: format!("{contract}: {detail}"),
227 };
228 let nonzero = |what: &str, address: Address| {
229 if address == Address::ZERO {
230 return Err(refuse(format!("{what} is the zero address")));
231 }
232 Ok(())
233 };
234 let capped = |what: &str, value: u64, limit: u64| {
235 if value > limit {
236 return Err(refuse(format!(
237 "{what} {value}s exceeds the ceiling {limit}s"
238 )));
239 }
240 Ok(())
241 };
242 match self {
243 Self::X(roots) | Self::GitHub(roots) => {
244 nonzero("owner", roots.owner)?;
245 nonzero("honk verifier", roots.honk_verifier)?;
246 if roots.notary_service == Address::ZERO {
247 return Err(refuse(
248 "notary service is the zero address, but the profile \
249 notarizes two sessions and must pin the Notary Service \
250 they are authenticated through"
251 .into(),
252 ));
253 }
254 capped("proof lifetime", roots.proof_lifetime, MAX_PROOF_LIFETIME)?;
255 capped(
256 "max future attestation skew",
257 roots.max_future_attestation_skew,
258 MAX_FUTURE_ATTESTATION_SKEW,
259 )?;
260 capped(
261 "future observation allowance",
262 roots.future_observation_allowance,
263 MAX_FUTURE_OBSERVATION_ALLOWANCE,
264 )
265 }
266 Self::Google(roots) => {
267 nonzero("owner", roots.owner)?;
268 nonzero("honk verifier", roots.honk_verifier)?;
269 nonzero("jwt roots", roots.jwt_roots)?;
270 capped(
271 "future observation allowance",
272 roots.future_observation_allowance,
273 MAX_FUTURE_OBSERVATION_ALLOWANCE,
274 )
275 }
276 }
277 }
278
279 pub async fn call<P: Provider>(&self, provider: &P) -> Result<InitializeCall> {
285 self.check()?;
286 let codehash =
287 codehash_at(provider, self.honk_verifier())
288 .await
289 .map_err(|e| Error::Initializer {
290 detail: format!("{}: honk verifier: {e}", self.verifier().contract()),
291 })?;
292 Ok(match self {
293 Self::X(roots) | Self::GitHub(roots) => {
294 InitializeCall::TlsNotary(TlsNotaryPlatformVerifier::initializeCall {
295 owner_: roots.owner,
296 notary_: roots.notary_service,
297 honkVerifier_: roots.honk_verifier,
298 honkVerifierCodehash_: codehash,
299 proofLifetime_: roots.proof_lifetime,
300 maxFutureAttestationSkew_: roots.max_future_attestation_skew,
301 futureObservationAllowance_: roots.future_observation_allowance,
302 })
303 }
304 Self::Google(roots) => {
305 InitializeCall::Google(GooglePlatformVerifier::initializeCall {
306 owner_: roots.owner,
307 notary_: Address::ZERO,
311 honkVerifier_: roots.honk_verifier,
312 honkVerifierCodehash_: codehash,
313 futureObservationAllowance_: roots.future_observation_allowance,
314 jwtRoots_: roots.jwt_roots,
315 })
316 }
317 })
318 }
319}
320
321pub async fn codehash_at<P: Provider>(provider: &P, address: Address) -> Result<B256> {
327 let code = provider
328 .get_code_at(address)
329 .await
330 .map_err(|e| Error::Rpc {
331 detail: format!("failed to read code at {address}: {e}"),
332 })?;
333 if code.is_empty() {
334 return Err(Error::Rpc {
335 detail: format!("no code at {address}"),
336 });
337 }
338 Ok(keccak256(&code))
339}
340
341pub async fn deploy_platform_verifier<P: Provider>(
349 provider: &P,
350 artifacts: &Artifacts,
351 init: &Initializer,
352 sender: Option<Address>,
353) -> Result<Address> {
354 let contract = init.verifier().contract();
355 match init.call(provider).await? {
356 InitializeCall::TlsNotary(call) => {
357 deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
358 }
359 InitializeCall::Google(call) => {
360 deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::artifacts::COVERED;
369
370 fn tls() -> TlsNotaryRoots {
371 TlsNotaryRoots {
372 owner: Address::repeat_byte(0x01),
373 notary_service: Address::repeat_byte(0x02),
374 honk_verifier: Address::repeat_byte(0x03),
375 proof_lifetime: 3600,
376 max_future_attestation_skew: 300,
377 future_observation_allowance: 300,
378 }
379 }
380
381 fn google() -> GoogleRoots {
382 GoogleRoots {
383 owner: Address::repeat_byte(0x01),
384 honk_verifier: Address::repeat_byte(0x03),
385 future_observation_allowance: 7200,
386 jwt_roots: Address::repeat_byte(0x04),
387 }
388 }
389
390 #[test]
394 fn notarizes_follows_the_profile_table() {
395 for verifier in PlatformVerifier::ALL {
396 let profile = libid_profiles::LAUNCH
397 .iter()
398 .find(|p| p.platform == verifier.platform())
399 .unwrap_or_else(|| panic!("{verifier:?} has no launch profile"));
400 assert_eq!(
401 verifier.notarizes(),
402 profile.attestation_count() != 0,
403 "{verifier:?}"
404 );
405 assert_eq!(verifier.platform_id(), keccak256(profile.platform));
406 }
407 assert_eq!(PlatformVerifier::ALL.len(), libid_profiles::LAUNCH.len());
408 }
409
410 #[test]
413 fn every_circuit_serves_a_platform() {
414 for circuit in Circuit::ALL {
415 assert!(
416 PlatformVerifier::ALL.iter().any(|v| v.circuit() == circuit),
417 "{circuit:?} serves no platform"
418 );
419 }
420 }
421
422 #[test]
424 fn every_verifier_is_covered() {
425 for verifier in PlatformVerifier::ALL {
426 let contract = verifier.contract();
427 assert!(
428 COVERED.contains(&(contract, contract)),
429 "{contract} is not in COVERED"
430 );
431 }
432 }
433
434 #[test]
435 fn well_formed_initializers_pass() {
436 Initializer::X(tls()).check().unwrap();
437 Initializer::GitHub(tls()).check().unwrap();
438 Initializer::Google(google()).check().unwrap();
439 }
440
441 #[test]
442 fn a_tls_notary_profile_must_pin_a_notary_service() {
443 let err = Initializer::GitHub(TlsNotaryRoots {
444 notary_service: Address::ZERO,
445 ..tls()
446 })
447 .check()
448 .unwrap_err();
449 assert!(matches!(err, Error::Initializer { .. }), "{err}");
450 assert!(err.to_string().contains("notary service"), "{err}");
451 assert!(err.to_string().contains("GitHubPlatformVerifier"), "{err}");
452 }
453
454 #[test]
455 fn parameters_over_their_ceilings_are_refused() {
456 let over = [
457 Initializer::X(TlsNotaryRoots {
458 proof_lifetime: MAX_PROOF_LIFETIME + 1,
459 ..tls()
460 }),
461 Initializer::X(TlsNotaryRoots {
462 max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW + 1,
463 ..tls()
464 }),
465 Initializer::X(TlsNotaryRoots {
466 future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
467 ..tls()
468 }),
469 Initializer::Google(GoogleRoots {
470 future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
471 ..google()
472 }),
473 ];
474 for init in over {
475 let err = init.check().unwrap_err();
476 assert!(err.to_string().contains("exceeds the ceiling"), "{err}");
477 }
478 Initializer::X(TlsNotaryRoots {
480 proof_lifetime: MAX_PROOF_LIFETIME,
481 max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW,
482 future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE,
483 ..tls()
484 })
485 .check()
486 .unwrap();
487 }
488
489 #[test]
490 fn zero_addresses_are_refused() {
491 let cases: [(Initializer, &str); 5] = [
492 (
493 Initializer::X(TlsNotaryRoots {
494 owner: Address::ZERO,
495 ..tls()
496 }),
497 "owner",
498 ),
499 (
500 Initializer::X(TlsNotaryRoots {
501 honk_verifier: Address::ZERO,
502 ..tls()
503 }),
504 "honk verifier",
505 ),
506 (
507 Initializer::Google(GoogleRoots {
508 owner: Address::ZERO,
509 ..google()
510 }),
511 "owner",
512 ),
513 (
514 Initializer::Google(GoogleRoots {
515 honk_verifier: Address::ZERO,
516 ..google()
517 }),
518 "honk verifier",
519 ),
520 (
521 Initializer::Google(GoogleRoots {
522 jwt_roots: Address::ZERO,
523 ..google()
524 }),
525 "jwt roots",
526 ),
527 ];
528 for (init, what) in cases {
529 let err = init.check().unwrap_err();
530 assert!(err.to_string().contains(what), "{what}: {err}");
531 }
532 }
533}