Skip to main content

libid_contracts/
platform_verifier.rs

1//! Deploying a launch Platform Verifier: which contract serves which
2//! platform, what it initializes with, and the rules its `initialize`
3//! enforces — checked here, off chain, before a transaction is built.
4//!
5//! `PlatformVerifierBase.__PlatformVerifierBase_init` refuses three things a
6//! deployer would otherwise rediscover at the proxy's constructor revert:
7//! a Notary Service that does not match what the profile notarizes (a
8//! TLSNotary profile must hold one, Google must hold none), a code hash
9//! that is zero, `keccak256("")` or not the hash of the code at the Honk
10//! verifier's address, and a zero owner or root list. The validity window is
11//! not among them: each verifier reads its profile's from `CeremonyProfile`,
12//! so a deployment supplies none. [`Initializer::call`] reads the code hash off the chain, checks
13//! the rest, and builds the exact `initialize` call;
14//! [`deploy_platform_verifier`] puts the implementation behind a fresh
15//! ERC1967 proxy with it.
16//!
17//! The Honk verifier a Platform Verifier pins is vendored here too
18//! ([`circuits`](crate::circuits)): bb-generated in `libid-circuits` from
19//! the circuit's verification key, deployed by
20//! [`deploy_honk_verifier`](crate::circuits::deploy_honk_verifier). Which
21//! circuit a platform proves under is [`PlatformVerifier::circuit`]; the
22//! contract pins whichever address governance names, by address AND by
23//! code hash.
24
25use alloy::{
26    primitives::{
27        keccak256,
28        Address,
29        B256,
30    },
31    providers::Provider,
32    sol_types::SolCall,
33};
34
35use crate::{
36    artifacts::Artifacts,
37    bindings::ceremony::{
38        GooglePlatformVerifier,
39        TlsNotaryPlatformVerifier,
40    },
41    circuits::Circuit,
42    deploy::deploy_behind_proxy,
43    error::{
44        Error,
45        Result,
46    },
47};
48
49/// One of the three launch Platform Verifiers.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum PlatformVerifier {
52    /// `x/v1`: `XPlatformVerifier`, a TLSNotary profile.
53    X,
54    /// `github/v1`: `GitHubPlatformVerifier`, a TLSNotary profile.
55    GitHub,
56    /// `google/v1`: `GooglePlatformVerifier`, a signed-token profile that
57    /// notarizes nothing.
58    Google,
59}
60
61impl PlatformVerifier {
62    /// Every launch verifier.
63    pub const ALL: [Self; 3] = [Self::X, Self::GitHub, Self::Google];
64
65    /// The contract, which is also its `.sol` file and its entry in
66    /// [`COVERED`](crate::artifacts::COVERED).
67    pub const fn contract(self) -> &'static str {
68        match self {
69            Self::X => "XPlatformVerifier",
70            Self::GitHub => "GitHubPlatformVerifier",
71            Self::Google => "GooglePlatformVerifier",
72        }
73    }
74
75    /// The platform's bare name, as `CeremonyProfile` spells it. libID
76    /// namespaces only its own strings.
77    pub const fn platform(self) -> &'static str {
78        match self {
79            Self::X => "x",
80            Self::GitHub => "github",
81            Self::Google => "google",
82        }
83    }
84
85    /// What the deployed contract answers to `platformId()`: `keccak256`
86    /// of the bare name.
87    pub fn platform_id(self) -> B256 {
88        keccak256(self.platform().as_bytes())
89    }
90
91    /// The ceremony circuit this platform's proofs are made under, and so
92    /// which vendored Honk verifier its `honk_verifier` should be.
93    pub const fn circuit(self) -> Circuit {
94        match self {
95            Self::X | Self::GitHub => Circuit::BearerLink,
96            Self::Google => Circuit::OidcGoogle,
97        }
98    }
99
100    /// Whether the profile notarizes any session, and so whether its
101    /// verifier holds a Notary Service. `CeremonyProfile.attestationCount`
102    /// is two for the TLSNotary profiles and zero for Google; the
103    /// `libid-profiles` table says the same, and a test pins the two
104    /// together.
105    pub const fn notarizes(self) -> bool {
106        match self {
107            Self::X | Self::GitHub => true,
108            Self::Google => false,
109        }
110    }
111}
112
113/// What a TLSNotary Platform Verifier (`x/v1`, `github/v1`) initializes
114/// with: its owner and trust roots.
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub struct TlsNotaryRoots {
117    /// Governance. Rotates the roots, upgrades.
118    pub owner: Address,
119    /// The Notary Service both attestations are authenticated through.
120    /// Required: the profile pins one (REQ-COMMON-18).
121    pub notary_service: Address,
122    /// The bb-generated UltraHonk verifier for this platform's circuit. Its
123    /// code hash is read off chain and pinned beside it.
124    pub honk_verifier: Address,
125}
126
127/// What the Google Platform Verifier initializes with. No Notary Service:
128/// the profile notarizes nothing, and the base refuses one.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub struct GoogleRoots {
131    /// Governance.
132    pub owner: Address,
133    /// The bb-generated UltraHonk verifier for the Google OIDC circuit.
134    pub honk_verifier: Address,
135    /// The `GoogleJwtRoots` proxy the trusted moduli are read through.
136    pub jwt_roots: Address,
137}
138
139/// What one Platform Verifier is initialized with.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum Initializer {
142    X(TlsNotaryRoots),
143    GitHub(TlsNotaryRoots),
144    Google(GoogleRoots),
145}
146
147/// A built `initialize` call, typed by shape. Feed
148/// [`abi_encode`](Self::abi_encode) to an ERC1967 proxy as its init data —
149/// through [`deploy_platform_verifier`], [`deploy_proxy`](crate::deploy::deploy_proxy),
150/// or as part of the creation code a [factory](crate::factory) deploy takes.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub enum InitializeCall {
153    /// `XPlatformVerifier.initialize` or `GitHubPlatformVerifier.initialize`
154    /// (one signature).
155    TlsNotary(TlsNotaryPlatformVerifier::initializeCall),
156    /// `GooglePlatformVerifier.initialize`.
157    Google(GooglePlatformVerifier::initializeCall),
158}
159
160impl InitializeCall {
161    /// The ABI-encoded call.
162    pub fn abi_encode(&self) -> Vec<u8> {
163        match self {
164            Self::TlsNotary(call) => call.abi_encode(),
165            Self::Google(call) => call.abi_encode(),
166        }
167    }
168
169    /// The code hash the call pins.
170    pub fn honk_verifier_codehash(&self) -> B256 {
171        match self {
172            Self::TlsNotary(call) => call.honkVerifierCodehash_,
173            Self::Google(call) => call.honkVerifierCodehash_,
174        }
175    }
176}
177
178impl Initializer {
179    /// Which verifier this initializes.
180    pub const fn verifier(&self) -> PlatformVerifier {
181        match self {
182            Self::X(_) => PlatformVerifier::X,
183            Self::GitHub(_) => PlatformVerifier::GitHub,
184            Self::Google(_) => PlatformVerifier::Google,
185        }
186    }
187
188    /// The Honk verifier it pins.
189    pub const fn honk_verifier(&self) -> Address {
190        match self {
191            Self::X(roots) | Self::GitHub(roots) => roots.honk_verifier,
192            Self::Google(roots) => roots.honk_verifier,
193        }
194    }
195
196    /// The rules `initialize` enforces that need no chain: a nonzero owner,
197    /// a nonzero Honk verifier, a Notary Service where the profile notarizes
198    /// (the Google shape cannot carry one at all), and a nonzero root list
199    /// for Google. The code hash is the one rule left to
200    /// [`call`](Self::call).
201    pub fn check(&self) -> Result<()> {
202        let contract = self.verifier().contract();
203        let refuse = |detail: String| Error::Initializer {
204            detail: format!("{contract}: {detail}"),
205        };
206        let nonzero = |what: &str, address: Address| {
207            if address == Address::ZERO {
208                return Err(refuse(format!("{what} is the zero address")));
209            }
210            Ok(())
211        };
212        match self {
213            Self::X(roots) | Self::GitHub(roots) => {
214                nonzero("owner", roots.owner)?;
215                nonzero("honk verifier", roots.honk_verifier)?;
216                if roots.notary_service == Address::ZERO {
217                    return Err(refuse(
218                        "notary service is the zero address, but the profile \
219                         notarizes two sessions and must pin the Notary Service \
220                         they are authenticated through"
221                            .into(),
222                    ));
223                }
224                Ok(())
225            }
226            Self::Google(roots) => {
227                nonzero("owner", roots.owner)?;
228                nonzero("honk verifier", roots.honk_verifier)?;
229                nonzero("jwt roots", roots.jwt_roots)
230            }
231        }
232    }
233
234    /// Build the `initialize` call: [`check`](Self::check), then read the
235    /// code hash of the Honk verifier through `provider` and pin it. Fails
236    /// when the address holds no code — the contract would refuse the
237    /// resulting hash, and a verifier that is not deployed yet is the
238    /// mis-wiring the check exists to catch.
239    pub async fn call<P: Provider>(&self, provider: &P) -> Result<InitializeCall> {
240        self.check()?;
241        let codehash =
242            codehash_at(provider, self.honk_verifier())
243                .await
244                .map_err(|e| Error::Initializer {
245                    detail: format!("{}: honk verifier: {e}", self.verifier().contract()),
246                })?;
247        Ok(match self {
248            Self::X(roots) | Self::GitHub(roots) => {
249                InitializeCall::TlsNotary(TlsNotaryPlatformVerifier::initializeCall {
250                    owner_: roots.owner,
251                    notary_: roots.notary_service,
252                    honkVerifier_: roots.honk_verifier,
253                    honkVerifierCodehash_: codehash,
254                })
255            }
256            Self::Google(roots) => {
257                InitializeCall::Google(GooglePlatformVerifier::initializeCall {
258                    owner_: roots.owner,
259                    // A profile whose Attestation Count is zero must not
260                    // reach a Notary Service (REQ-COMMON-05D); the base
261                    // refuses one.
262                    notary_: Address::ZERO,
263                    honkVerifier_: roots.honk_verifier,
264                    honkVerifierCodehash_: codehash,
265                    jwtRoots_: roots.jwt_roots,
266                })
267            }
268        })
269    }
270}
271
272/// The code hash of the account at `address`, as `EXTCODEHASH` reports it
273/// for an account with code: `keccak256` of its runtime bytecode. An
274/// account without code is an error rather than `keccak256("")` or zero,
275/// because `setTrustRoots` refuses both and a caller comparing against the
276/// hash of nothing has nothing to pin.
277pub async fn codehash_at<P: Provider>(provider: &P, address: Address) -> Result<B256> {
278    let code = provider
279        .get_code_at(address)
280        .await
281        .map_err(|e| Error::Rpc {
282            detail: format!("failed to read code at {address}: {e}"),
283        })?;
284    if code.is_empty() {
285        return Err(Error::Rpc {
286            detail: format!("no code at {address}"),
287        });
288    }
289    Ok(keccak256(&code))
290}
291
292/// Deploy the verifier's implementation from `artifacts` and put it behind
293/// a fresh ERC1967 proxy initialized with `init` — the code hash read off
294/// the chain, the rules checked first. Returns the proxy address, which is
295/// the Platform Verifier a Proof Verifier registers with `setVerifier`.
296///
297/// `sender` opts into explicit nonce management (see
298/// [`deploy_contract_from`](crate::deploy::deploy_contract_from)).
299pub async fn deploy_platform_verifier<P: Provider>(
300    provider: &P,
301    artifacts: &Artifacts,
302    init: &Initializer,
303    sender: Option<Address>,
304) -> Result<Address> {
305    let contract = init.verifier().contract();
306    match init.call(provider).await? {
307        InitializeCall::TlsNotary(call) => {
308            deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
309        }
310        InitializeCall::Google(call) => {
311            deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::artifacts::COVERED;
320
321    fn tls() -> TlsNotaryRoots {
322        TlsNotaryRoots {
323            owner: Address::repeat_byte(0x01),
324            notary_service: Address::repeat_byte(0x02),
325            honk_verifier: Address::repeat_byte(0x03),
326        }
327    }
328
329    fn google() -> GoogleRoots {
330        GoogleRoots {
331            owner: Address::repeat_byte(0x01),
332            honk_verifier: Address::repeat_byte(0x03),
333            jwt_roots: Address::repeat_byte(0x04),
334        }
335    }
336
337    /// Whether a verifier holds a Notary Service is derived from the
338    /// generated profile table, as the contract derives it from
339    /// `CeremonyProfile.attestationCount`.
340    #[test]
341    fn notarizes_follows_the_profile_table() {
342        for verifier in PlatformVerifier::ALL {
343            let profile = libid_profiles::LAUNCH
344                .iter()
345                .find(|p| p.platform == verifier.platform())
346                .unwrap_or_else(|| panic!("{verifier:?} has no launch profile"));
347            assert_eq!(
348                verifier.notarizes(),
349                profile.attestation_count() != 0,
350                "{verifier:?}"
351            );
352            assert_eq!(verifier.platform_id(), keccak256(profile.platform));
353        }
354        assert_eq!(PlatformVerifier::ALL.len(), libid_profiles::LAUNCH.len());
355    }
356
357    /// Every circuit has a platform proving under it: a vendored verifier
358    /// no platform pins would be dead weight in every consumer's binary.
359    #[test]
360    fn every_circuit_serves_a_platform() {
361        for circuit in Circuit::ALL {
362            assert!(
363                PlatformVerifier::ALL.iter().any(|v| v.circuit() == circuit),
364                "{circuit:?} serves no platform"
365            );
366        }
367    }
368
369    /// Every verifier's contract is one the crate vendors.
370    #[test]
371    fn every_verifier_is_covered() {
372        for verifier in PlatformVerifier::ALL {
373            let contract = verifier.contract();
374            assert!(
375                COVERED.contains(&(contract, contract)),
376                "{contract} is not in COVERED"
377            );
378        }
379    }
380
381    #[test]
382    fn well_formed_initializers_pass() {
383        Initializer::X(tls()).check().unwrap();
384        Initializer::GitHub(tls()).check().unwrap();
385        Initializer::Google(google()).check().unwrap();
386    }
387
388    #[test]
389    fn a_tls_notary_profile_must_pin_a_notary_service() {
390        let err = Initializer::GitHub(TlsNotaryRoots {
391            notary_service: Address::ZERO,
392            ..tls()
393        })
394        .check()
395        .unwrap_err();
396        assert!(matches!(err, Error::Initializer { .. }), "{err}");
397        assert!(err.to_string().contains("notary service"), "{err}");
398        assert!(err.to_string().contains("GitHubPlatformVerifier"), "{err}");
399    }
400
401    #[test]
402    fn zero_addresses_are_refused() {
403        let cases: [(Initializer, &str); 5] = [
404            (
405                Initializer::X(TlsNotaryRoots {
406                    owner: Address::ZERO,
407                    ..tls()
408                }),
409                "owner",
410            ),
411            (
412                Initializer::X(TlsNotaryRoots {
413                    honk_verifier: Address::ZERO,
414                    ..tls()
415                }),
416                "honk verifier",
417            ),
418            (
419                Initializer::Google(GoogleRoots {
420                    owner: Address::ZERO,
421                    ..google()
422                }),
423                "owner",
424            ),
425            (
426                Initializer::Google(GoogleRoots {
427                    honk_verifier: Address::ZERO,
428                    ..google()
429                }),
430                "honk verifier",
431            ),
432            (
433                Initializer::Google(GoogleRoots {
434                    jwt_roots: Address::ZERO,
435                    ..google()
436                }),
437                "jwt roots",
438            ),
439        ];
440        for (init, what) in cases {
441            let err = init.check().unwrap_err();
442            assert!(err.to_string().contains(what), "{what}: {err}");
443        }
444    }
445}