use alloy::{
primitives::{
keccak256,
Address,
B256,
},
providers::Provider,
sol_types::SolCall,
};
use crate::{
artifacts::Artifacts,
bindings::ceremony::{
GooglePlatformVerifier,
TlsNotaryPlatformVerifier,
},
circuits::Circuit,
deploy::deploy_behind_proxy,
error::{
Error,
Result,
},
};
pub const MAX_PROOF_LIFETIME: u64 = 30 * 24 * 60 * 60;
pub const MAX_FUTURE_ATTESTATION_SKEW: u64 = 24 * 60 * 60;
pub const MAX_FUTURE_OBSERVATION_ALLOWANCE: u64 = 24 * 60 * 60;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PlatformVerifier {
X,
GitHub,
Google,
}
impl PlatformVerifier {
pub const ALL: [Self; 3] = [Self::X, Self::GitHub, Self::Google];
pub const fn contract(self) -> &'static str {
match self {
Self::X => "XPlatformVerifier",
Self::GitHub => "GitHubPlatformVerifier",
Self::Google => "GooglePlatformVerifier",
}
}
pub const fn platform(self) -> &'static str {
match self {
Self::X => "x",
Self::GitHub => "github",
Self::Google => "google",
}
}
pub fn platform_id(self) -> B256 {
keccak256(self.platform().as_bytes())
}
pub const fn circuit(self) -> Circuit {
match self {
Self::X | Self::GitHub => Circuit::BearerLink,
Self::Google => Circuit::OidcGoogle,
}
}
pub const fn notarizes(self) -> bool {
match self {
Self::X | Self::GitHub => true,
Self::Google => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TlsNotaryRoots {
pub owner: Address,
pub notary_service: Address,
pub honk_verifier: Address,
pub proof_lifetime: u64,
pub max_future_attestation_skew: u64,
pub future_observation_allowance: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GoogleRoots {
pub owner: Address,
pub honk_verifier: Address,
pub future_observation_allowance: u64,
pub jwt_roots: Address,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Initializer {
X(TlsNotaryRoots),
GitHub(TlsNotaryRoots),
Google(GoogleRoots),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum InitializeCall {
TlsNotary(TlsNotaryPlatformVerifier::initializeCall),
Google(GooglePlatformVerifier::initializeCall),
}
impl InitializeCall {
pub fn abi_encode(&self) -> Vec<u8> {
match self {
Self::TlsNotary(call) => call.abi_encode(),
Self::Google(call) => call.abi_encode(),
}
}
pub fn honk_verifier_codehash(&self) -> B256 {
match self {
Self::TlsNotary(call) => call.honkVerifierCodehash_,
Self::Google(call) => call.honkVerifierCodehash_,
}
}
}
impl Initializer {
pub const fn verifier(&self) -> PlatformVerifier {
match self {
Self::X(_) => PlatformVerifier::X,
Self::GitHub(_) => PlatformVerifier::GitHub,
Self::Google(_) => PlatformVerifier::Google,
}
}
pub const fn honk_verifier(&self) -> Address {
match self {
Self::X(roots) | Self::GitHub(roots) => roots.honk_verifier,
Self::Google(roots) => roots.honk_verifier,
}
}
pub fn check(&self) -> Result<()> {
let contract = self.verifier().contract();
let refuse = |detail: String| Error::Initializer {
detail: format!("{contract}: {detail}"),
};
let nonzero = |what: &str, address: Address| {
if address == Address::ZERO {
return Err(refuse(format!("{what} is the zero address")));
}
Ok(())
};
let capped = |what: &str, value: u64, limit: u64| {
if value > limit {
return Err(refuse(format!(
"{what} {value}s exceeds the ceiling {limit}s"
)));
}
Ok(())
};
match self {
Self::X(roots) | Self::GitHub(roots) => {
nonzero("owner", roots.owner)?;
nonzero("honk verifier", roots.honk_verifier)?;
if roots.notary_service == Address::ZERO {
return Err(refuse(
"notary service is the zero address, but the profile \
notarizes two sessions and must pin the Notary Service \
they are authenticated through"
.into(),
));
}
capped("proof lifetime", roots.proof_lifetime, MAX_PROOF_LIFETIME)?;
capped(
"max future attestation skew",
roots.max_future_attestation_skew,
MAX_FUTURE_ATTESTATION_SKEW,
)?;
capped(
"future observation allowance",
roots.future_observation_allowance,
MAX_FUTURE_OBSERVATION_ALLOWANCE,
)
}
Self::Google(roots) => {
nonzero("owner", roots.owner)?;
nonzero("honk verifier", roots.honk_verifier)?;
nonzero("jwt roots", roots.jwt_roots)?;
capped(
"future observation allowance",
roots.future_observation_allowance,
MAX_FUTURE_OBSERVATION_ALLOWANCE,
)
}
}
}
pub async fn call<P: Provider>(&self, provider: &P) -> Result<InitializeCall> {
self.check()?;
let codehash =
codehash_at(provider, self.honk_verifier())
.await
.map_err(|e| Error::Initializer {
detail: format!("{}: honk verifier: {e}", self.verifier().contract()),
})?;
Ok(match self {
Self::X(roots) | Self::GitHub(roots) => {
InitializeCall::TlsNotary(TlsNotaryPlatformVerifier::initializeCall {
owner_: roots.owner,
notary_: roots.notary_service,
honkVerifier_: roots.honk_verifier,
honkVerifierCodehash_: codehash,
proofLifetime_: roots.proof_lifetime,
maxFutureAttestationSkew_: roots.max_future_attestation_skew,
futureObservationAllowance_: roots.future_observation_allowance,
})
}
Self::Google(roots) => {
InitializeCall::Google(GooglePlatformVerifier::initializeCall {
owner_: roots.owner,
notary_: Address::ZERO,
honkVerifier_: roots.honk_verifier,
honkVerifierCodehash_: codehash,
futureObservationAllowance_: roots.future_observation_allowance,
jwtRoots_: roots.jwt_roots,
})
}
})
}
}
pub async fn codehash_at<P: Provider>(provider: &P, address: Address) -> Result<B256> {
let code = provider
.get_code_at(address)
.await
.map_err(|e| Error::Rpc {
detail: format!("failed to read code at {address}: {e}"),
})?;
if code.is_empty() {
return Err(Error::Rpc {
detail: format!("no code at {address}"),
});
}
Ok(keccak256(&code))
}
pub async fn deploy_platform_verifier<P: Provider>(
provider: &P,
artifacts: &Artifacts,
init: &Initializer,
sender: Option<Address>,
) -> Result<Address> {
let contract = init.verifier().contract();
match init.call(provider).await? {
InitializeCall::TlsNotary(call) => {
deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
}
InitializeCall::Google(call) => {
deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::artifacts::COVERED;
fn tls() -> TlsNotaryRoots {
TlsNotaryRoots {
owner: Address::repeat_byte(0x01),
notary_service: Address::repeat_byte(0x02),
honk_verifier: Address::repeat_byte(0x03),
proof_lifetime: 3600,
max_future_attestation_skew: 300,
future_observation_allowance: 300,
}
}
fn google() -> GoogleRoots {
GoogleRoots {
owner: Address::repeat_byte(0x01),
honk_verifier: Address::repeat_byte(0x03),
future_observation_allowance: 7200,
jwt_roots: Address::repeat_byte(0x04),
}
}
#[test]
fn notarizes_follows_the_profile_table() {
for verifier in PlatformVerifier::ALL {
let profile = libid_profiles::LAUNCH
.iter()
.find(|p| p.platform == verifier.platform())
.unwrap_or_else(|| panic!("{verifier:?} has no launch profile"));
assert_eq!(
verifier.notarizes(),
profile.attestation_count() != 0,
"{verifier:?}"
);
assert_eq!(verifier.platform_id(), keccak256(profile.platform));
}
assert_eq!(PlatformVerifier::ALL.len(), libid_profiles::LAUNCH.len());
}
#[test]
fn every_circuit_serves_a_platform() {
for circuit in Circuit::ALL {
assert!(
PlatformVerifier::ALL.iter().any(|v| v.circuit() == circuit),
"{circuit:?} serves no platform"
);
}
}
#[test]
fn every_verifier_is_covered() {
for verifier in PlatformVerifier::ALL {
let contract = verifier.contract();
assert!(
COVERED.contains(&(contract, contract)),
"{contract} is not in COVERED"
);
}
}
#[test]
fn well_formed_initializers_pass() {
Initializer::X(tls()).check().unwrap();
Initializer::GitHub(tls()).check().unwrap();
Initializer::Google(google()).check().unwrap();
}
#[test]
fn a_tls_notary_profile_must_pin_a_notary_service() {
let err = Initializer::GitHub(TlsNotaryRoots {
notary_service: Address::ZERO,
..tls()
})
.check()
.unwrap_err();
assert!(matches!(err, Error::Initializer { .. }), "{err}");
assert!(err.to_string().contains("notary service"), "{err}");
assert!(err.to_string().contains("GitHubPlatformVerifier"), "{err}");
}
#[test]
fn parameters_over_their_ceilings_are_refused() {
let over = [
Initializer::X(TlsNotaryRoots {
proof_lifetime: MAX_PROOF_LIFETIME + 1,
..tls()
}),
Initializer::X(TlsNotaryRoots {
max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW + 1,
..tls()
}),
Initializer::X(TlsNotaryRoots {
future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
..tls()
}),
Initializer::Google(GoogleRoots {
future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
..google()
}),
];
for init in over {
let err = init.check().unwrap_err();
assert!(err.to_string().contains("exceeds the ceiling"), "{err}");
}
Initializer::X(TlsNotaryRoots {
proof_lifetime: MAX_PROOF_LIFETIME,
max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW,
future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE,
..tls()
})
.check()
.unwrap();
}
#[test]
fn zero_addresses_are_refused() {
let cases: [(Initializer, &str); 5] = [
(
Initializer::X(TlsNotaryRoots {
owner: Address::ZERO,
..tls()
}),
"owner",
),
(
Initializer::X(TlsNotaryRoots {
honk_verifier: Address::ZERO,
..tls()
}),
"honk verifier",
),
(
Initializer::Google(GoogleRoots {
owner: Address::ZERO,
..google()
}),
"owner",
),
(
Initializer::Google(GoogleRoots {
honk_verifier: Address::ZERO,
..google()
}),
"honk verifier",
),
(
Initializer::Google(GoogleRoots {
jwt_roots: Address::ZERO,
..google()
}),
"jwt roots",
),
];
for (init, what) in cases {
let err = init.check().unwrap_err();
assert!(err.to_string().contains(what), "{what}: {err}");
}
}
}