From aea1ebf47829ea75aa4677e5879c0a9e8c354433 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 16:34:52 +0100 Subject: [PATCH 1/3] feat(contracts): vendor and bind the three Platform Verifiers The crate covered NotaryService, CeremonyProofVerifier and GoogleJwtRoots but not the Platform Verifiers they route to, so a deployer had to carry its own copies of the artifacts and sol! interfaces. Vendor the three and bind them: one interface for the two TLSNotary verifiers, which share a surface, and one for Google, whose initializer differs in shape. A unit test pins every bound selector to the vendored methodIdentifiers. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/src/artifacts.rs | 5 + rust/contracts/src/bindings/ceremony.rs | 275 +++++++++++++++++++++++- rust/contracts/src/lib.rs | 9 +- scripts/vendor-artifacts.sh | 5 + 4 files changed, 289 insertions(+), 5 deletions(-) diff --git a/rust/contracts/src/artifacts.rs b/rust/contracts/src/artifacts.rs index 51feca5..264813d 100644 --- a/rust/contracts/src/artifacts.rs +++ b/rust/contracts/src/artifacts.rs @@ -39,6 +39,11 @@ pub const COVERED: &[(&str, &str)] = &[ ("CeremonyProofVerifier", "CeremonyProofVerifier"), ("ERC1967Proxy", "ERC1967Proxy"), ("GoogleJwtRoots", "GoogleJwtRoots"), + // ceremony: the launch Platform Verifiers (one per profile; the UltraHonk + // verifier each pins comes from the circuits release, not from here) + ("XPlatformVerifier", "XPlatformVerifier"), + ("GitHubPlatformVerifier", "GitHubPlatformVerifier"), + ("GooglePlatformVerifier", "GooglePlatformVerifier"), // identity ("IdentityNames", "IdentityNames"), // ens (deployed once per network, not CREATE3-canonical) diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index b9a4209..abe9c63 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -1,7 +1,14 @@ //! Bindings for the ceremony verification path (`solidity/contracts/ceremony/`): //! the Notary Service every notarized session is authenticated through, the //! Proof Verifier that routes a claim to the Platform Verifier registered for -//! its version, and the Google JWT root list the `google/v1` verifier reads. +//! its version, the three launch Platform Verifiers it routes to, and the +//! Google JWT root list the `google/v1` verifier reads. +//! +//! `verify` is on none of the Platform Verifier interfaces, for the reason it +//! is on neither `NotaryService` nor `CeremonyProofVerifier`: a contract on +//! the route calls it with the fee attached, and the decoded claim comes back +//! to that contract. What an operator does from here is initialize, rotate +//! the trust roots and move the governance parameters. /// Bindings for `ceremony/NotaryService.sol` (which implements /// `INotaryService`). @@ -171,3 +178,269 @@ mod google_jwt_roots_inner { } pub use google_jwt_roots_inner::GoogleJwtRoots; + +/// Bindings for the two TLSNotary Platform Verifiers, `ceremony/XPlatformVerifier.sol` +/// and `ceremony/GitHubPlatformVerifier.sol`. One interface serves both: they +/// differ in the revealed layout they accept, not in the surface an operator +/// touches, and each answers for itself through `platformId()`. The same +/// module is exported as [`XPlatformVerifier`] and [`GitHubPlatformVerifier`], +/// so a consumer names the contract it means. +/// +/// The initializer takes what `PlatformVerifierBase.__PlatformVerifierBase_init` +/// takes. `notary_` is required here (nonzero): a TLSNotary profile +/// authenticates two attestations through it, and the base refuses a zero +/// address for a profile whose Attestation Count is nonzero +/// (`WrongNotaryForProfile`). `honkVerifierCodehash_` must equal +/// `address(honkVerifier_).codehash` and be neither zero nor `keccak256("")` +/// (`WrongVerifierArtifact`); the three parameters are capped by the `MAX_*` +/// constants (`ParameterTooLarge`). +#[allow(clippy::too_many_arguments, unused_attributes)] +mod tls_notary_platform_verifier_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface TlsNotaryPlatformVerifier { + function initialize( + address owner_, + address notary_, + address honkVerifier_, + bytes32 honkVerifierCodehash_, + uint64 proofLifetime_, + uint64 maxFutureAttestationSkew_, + uint64 futureObservationAllowance_ + ) external; + + /// The identity platform this verifier serves: `keccak256` of the + /// platform's bare name. The Proof Verifier refuses to register it + /// under another platform. + function platformId() external view returns (bytes32); + /// What a submission must carry: one Notary Fee per attestation + /// the profile requires — two, for a TLSNotary profile. + function quote() external view returns (uint256); + + function notaryService() external view returns (address); + function honkVerifier() external view returns (address); + /// The code hash of the artifact wired: the only handle on WHICH + /// circuit a deployed bb verifier answers for. + function honkVerifierCodehash() external view returns (bytes32); + function protocolParameters() + external + view + returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance); + /// Rotate the trust roots. The same rules as `initialize`: the + /// code hash names the artifact, and the call fails if the + /// address does not hold it. + function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external; + function setProtocolParameters( + uint64 proofLifetime_, + uint64 maxFutureAttestationSkew_, + uint64 futureObservationAllowance_ + ) external; + + /// Ceilings on the three parameters, in seconds. + function MAX_PROOF_LIFETIME() external view returns (uint64); + function MAX_FUTURE_ATTESTATION_SKEW() external view returns (uint64); + function MAX_FUTURE_OBSERVATION_ALLOWANCE() external view returns (uint64); + + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash); + event ProtocolParametersChanged( + uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance + ); + + /// A profile that verifies no attestation holds a Notary Service, + /// or one that verifies some holds none. + error WrongNotaryForProfile(bytes32 platformId, address notary); + error ParameterTooLarge(uint64 provided, uint64 limit); + error ZeroAddress(); + /// The verifier at that address is not the artifact named. + error WrongVerifierArtifact(bytes32 expected, bytes32 found); + } + } +} + +pub use tls_notary_platform_verifier_inner::{ + TlsNotaryPlatformVerifier, + TlsNotaryPlatformVerifier as GitHubPlatformVerifier, + TlsNotaryPlatformVerifier as XPlatformVerifier, +}; + +/// Bindings for `ceremony/GooglePlatformVerifier.sol` — the `google/v1` +/// profile. +/// +/// A different shape from the other two: no notarized session, so no Notary +/// Service, no fee, no proof lifetime and no attestation skew. The evidence +/// is a signed ID Token whose `exp` is the whole validity ceiling, and the +/// signing keys it trusts are read from `GoogleJwtRoots`. +/// +/// `notary_` must therefore be the ZERO address: the base refuses a Notary +/// Service for a profile whose Attestation Count is zero +/// (`WrongNotaryForProfile`), because `notaryService()` would otherwise +/// report a collaborator nothing on this path calls. `jwtRoots_` must be +/// nonzero (`ZeroAddress`). The code hash and the allowance follow the same +/// rules as the TLSNotary verifiers'; the lifetime and skew read back as +/// zero. +#[allow(clippy::too_many_arguments, unused_attributes)] +mod google_platform_verifier_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface GooglePlatformVerifier { + function initialize( + address owner_, + address notary_, + address honkVerifier_, + bytes32 honkVerifierCodehash_, + uint64 futureObservationAllowance_, + address jwtRoots_ + ) external; + + /// `keccak256("google")`. + function platformId() external view returns (bytes32); + /// Always zero: the profile verifies nothing that charges, and + /// `verify` refuses any value sent. + function quote() external view returns (uint256); + + /// The root list the trusted moduli are read through. + function jwtRoots() external view returns (address); + function setJwtRoots(address roots) external; + + function notaryService() external view returns (address); + function honkVerifier() external view returns (address); + function honkVerifierCodehash() external view returns (bytes32); + function protocolParameters() + external + view + returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance); + function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external; + function setProtocolParameters( + uint64 proofLifetime_, + uint64 maxFutureAttestationSkew_, + uint64 futureObservationAllowance_ + ) external; + + function MAX_PROOF_LIFETIME() external view returns (uint64); + function MAX_FUTURE_ATTESTATION_SKEW() external view returns (uint64); + function MAX_FUTURE_OBSERVATION_ALLOWANCE() external view returns (uint64); + + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event JwtRootsChanged(address roots); + event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash); + event ProtocolParametersChanged( + uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance + ); + + error WrongNotaryForProfile(bytes32 platformId, address notary); + error ParameterTooLarge(uint64 provided, uint64 limit); + error ZeroAddress(); + error WrongVerifierArtifact(bytes32 expected, bytes32 found); + } + } +} + +pub use google_platform_verifier_inner::GooglePlatformVerifier; + +#[cfg(test)] +mod tests { + use alloy::sol_types::SolCall; + + use super::*; + use crate::Artifacts; + + /// A Platform Verifier binding and its vendored artifact come from one + /// tree, so every bound selector is one the compiled contract answers. + /// The initializer is the one that matters: an `initialize` the proxy's + /// implementation has no function for reaches its fallback, and the + /// proxy is left uninitialized for anyone to claim. + #[test] + fn every_bound_platform_verifier_selector_exists_in_its_artifact() { + let artifacts = Artifacts::embedded(); + let check = |contract: &str, sig: &str, selector: [u8; 4]| { + let methods = artifacts.method_identifiers(contract).unwrap(); + let found = methods + .get(sig) + .unwrap_or_else(|| panic!("{contract} has no {sig}")); + assert_eq!(*found, alloy::hex::encode(selector), "{contract}.{sig}"); + }; + + macro_rules! bound { + ($contract:expr, $iface:ident, [$($call:ident),* $(,)?]) => { + $(check($contract, $iface::$call::SIGNATURE, $iface::$call::SELECTOR);)* + }; + } + + for contract in ["XPlatformVerifier", "GitHubPlatformVerifier"] { + bound!( + contract, + TlsNotaryPlatformVerifier, + [ + initializeCall, + platformIdCall, + quoteCall, + notaryServiceCall, + honkVerifierCall, + honkVerifierCodehashCall, + protocolParametersCall, + setTrustRootsCall, + setProtocolParametersCall, + MAX_PROOF_LIFETIMECall, + MAX_FUTURE_ATTESTATION_SKEWCall, + MAX_FUTURE_OBSERVATION_ALLOWANCECall, + ownerCall, + pendingOwnerCall, + transferOwnershipCall, + acceptOwnershipCall, + ] + ); + } + bound!( + "GooglePlatformVerifier", + GooglePlatformVerifier, + [ + initializeCall, + platformIdCall, + quoteCall, + jwtRootsCall, + setJwtRootsCall, + notaryServiceCall, + honkVerifierCall, + honkVerifierCodehashCall, + protocolParametersCall, + setTrustRootsCall, + setProtocolParametersCall, + MAX_PROOF_LIFETIMECall, + MAX_FUTURE_ATTESTATION_SKEWCall, + MAX_FUTURE_OBSERVATION_ALLOWANCECall, + ownerCall, + pendingOwnerCall, + transferOwnershipCall, + acceptOwnershipCall, + ] + ); + + // The two initializers differ in shape, and the artifacts say so: + // the TLSNotary one is not on Google's contract, nor the reverse. + assert_ne!( + TlsNotaryPlatformVerifier::initializeCall::SELECTOR, + GooglePlatformVerifier::initializeCall::SELECTOR + ); + let google = artifacts + .method_identifiers("GooglePlatformVerifier") + .unwrap(); + assert!( + !google.contains_key(TlsNotaryPlatformVerifier::initializeCall::SIGNATURE) + ); + let x = artifacts.method_identifiers("XPlatformVerifier").unwrap(); + assert!(!x.contains_key(GooglePlatformVerifier::initializeCall::SIGNATURE)); + } +} diff --git a/rust/contracts/src/lib.rs b/rust/contracts/src/lib.rs index a4c4196..5b9884c 100644 --- a/rust/contracts/src/lib.rs +++ b/rust/contracts/src/lib.rs @@ -5,10 +5,11 @@ //! //! - [`bindings`] — hand-written `alloy::sol!` interfaces for every contract a //! consumer talks to: the ceremony verification path (`NotaryService`, -//! `CeremonyProofVerifier`, and `GoogleJwtRoots`, the Google signing keys -//! the `google/v1` verifier trusts), the naming system (`IdentityNames`), -//! and the deterministic factory. Kept in lockstep with the Solidity -//! sources in `solidity/contracts`. +//! `CeremonyProofVerifier`, the three launch Platform Verifiers it routes +//! to, and `GoogleJwtRoots`, the Google signing keys the `google/v1` +//! verifier trusts), the naming system (`IdentityNames`), and the +//! deterministic factory. Kept in lockstep with the Solidity sources in +//! `solidity/contracts`. //! - [`artifacts`] — the compiled creation bytecode, link references, and //! method identifiers of every deployable contract, embedded at compile time //! ([`Artifacts::embedded`]) so deployment needs no filesystem at runtime. A diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index a60c817..c5bf008 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -33,6 +33,11 @@ ARTIFACTS=( "CeremonyProofVerifier:CeremonyProofVerifier" "ERC1967Proxy:ERC1967Proxy" "GoogleJwtRoots:GoogleJwtRoots" + # ceremony: the launch Platform Verifiers (one per profile; the UltraHonk + # verifier each pins comes from the circuits release, not from here) + "XPlatformVerifier:XPlatformVerifier" + "GitHubPlatformVerifier:GitHubPlatformVerifier" + "GooglePlatformVerifier:GooglePlatformVerifier" # identity "IdentityNames:IdentityNames" # ens (deployed once per network, not CREATE3-canonical; embedded so a From 52bc0131c5a8a5e8697fe4e78ed17e750341e37d Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 16:38:22 +0100 Subject: [PATCH 2/3] feat(contracts): ship the Platform Verifier initializer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlatformVerifierBase refuses a Notary Service that disagrees with what the profile notarizes, a code hash that is not the Honk verifier's, and a parameter over its ceiling — rules a deployer met as an opaque proxy constructor revert. `platform_verifier::Initializer` checks them off chain, reads the code hash, and builds the exact `initialize` call; `deploy_platform_verifier` puts the implementation behind a proxy with it. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/Cargo.lock | 1 + rust/contracts/Cargo.toml | 6 +- rust/contracts/README.md | 69 +++- rust/contracts/src/bindings/ceremony.rs | 8 +- rust/contracts/src/error.rs | 7 + rust/contracts/src/lib.rs | 5 + rust/contracts/src/platform_verifier.rs | 508 ++++++++++++++++++++++++ 7 files changed, 598 insertions(+), 6 deletions(-) create mode 100644 rust/contracts/src/platform_verifier.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a582613..d230deb 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2562,6 +2562,7 @@ version = "0.10.0" dependencies = [ "alloy", "include_dir", + "libid-profiles", "serde", "serde_json", "thiserror", diff --git a/rust/contracts/Cargo.toml b/rust/contracts/Cargo.toml index 689c53d..ea24cd0 100644 --- a/rust/contracts/Cargo.toml +++ b/rust/contracts/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" rust-version = "1.94.1" license = "MIT OR Apache-2.0" repository = "https://github.com/libid-org/libid-contracts" -description = "Typed alloy bindings, embedded forge artifacts, and deploy/upgrade helpers for the libid identity stack (Notary Service, ceremony proof verifier, Google JWT roots, identity names, deterministic factory)." +description = "Typed alloy bindings, embedded forge artifacts, and deploy/upgrade helpers for the libid identity stack (Notary Service, ceremony proof verifier, Platform Verifiers, Google JWT roots, identity names, deterministic factory)." keywords = ["ethereum", "alloy", "contracts", "deployment"] categories = ["cryptography::cryptocurrencies"] # The vendored artifacts are part of the crate: consumers deploy from the @@ -30,4 +30,8 @@ tokio = { version = "1", features = ["time"] } [dev-dependencies] alloy = { version = "1", features = ["node-bindings", "signer-local"] } +# Tests only, path only: the crate restates which profiles notarize so it +# publishes without a dependency on the sibling crate, and this is what pins +# the restatement to the generated table. +libid-profiles = { path = "../profiles" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/rust/contracts/README.md b/rust/contracts/README.md index 895c189..9687ba2 100644 --- a/rust/contracts/README.md +++ b/rust/contracts/README.md @@ -2,10 +2,10 @@ Typed [alloy](https://github.com/alloy-rs/alloy) bindings, embedded forge artifacts, and deploy/upgrade helpers for the libid identity stack: the -ceremony verification path (`NotaryService`, `CeremonyProofVerifier`, and -`GoogleJwtRoots`, the signing keys the `google/v1` verifier trusts), the -naming system (`IdentityNames`), and the deterministic deployment factory -(`LibidFactory`). +ceremony verification path (`NotaryService`, `CeremonyProofVerifier`, the +three launch Platform Verifiers it routes to, and `GoogleJwtRoots`, the +signing keys the `google/v1` verifier trusts), the naming system +(`IdentityNames`), and the deterministic deployment factory (`LibidFactory`). The compiled artifacts are vendored into the crate, so a consumer can deploy or upgrade the whole stack against a live network with **zero filesystem @@ -74,6 +74,65 @@ async fn main() -> Result<(), Box> { } ``` +## Example: deploy a Platform Verifier + +A Platform Verifier pins the bb-generated UltraHonk verifier for its circuit +by address and by code hash, holds a Notary Service only if its profile +notarizes anything, and caps its parameters. `platform_verifier::Initializer` +knows those rules: it reads the code hash off the chain, refuses what the +contract would refuse, and builds the exact `initialize` call. The Honk +verifier comes from a `libid-circuits` release and is deployed beforehand. + +```rust,no_run +use alloy::{primitives::Address, providers::ProviderBuilder}; +use libid_contracts::{ + bindings::ceremony::{CeremonyProofVerifier, XPlatformVerifier}, + platform_verifier::{deploy_platform_verifier, Initializer, TlsNotaryRoots}, + Artifacts, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let provider = ProviderBuilder::new() + .wallet(/* your signer */ todo!()) + .connect_http("https://rpc.example.org".parse()?); + let artifacts = Artifacts::embedded(); + let (owner, notary, honk_verifier, proof_verifier): (Address, Address, Address, Address) = + todo!(); + + // `x/v1`: two notarized sessions, so a Notary Service is required. + // `Initializer::Google` takes `GoogleRoots` instead — no Notary Service + // (the profile notarizes nothing) and the JWT root list in its place. + let verifier = deploy_platform_verifier( + &provider, + &artifacts, + &Initializer::X(TlsNotaryRoots { + owner, + notary_service: notary, + honk_verifier, + proof_lifetime: 3600, + max_future_attestation_skew: 300, + future_observation_allowance: 300, + }), + None, + ) + .await?; + + // Register it for the platform's launch slot. + let platform_id = XPlatformVerifier::new(verifier, &provider).platformId().call().await?; + CeremonyProofVerifier::new(proof_verifier, &provider) + .setVerifier(platform_id, 1, verifier) + .send() + .await? + .get_receipt() + .await?; + Ok(()) +} +``` + +For a factory (CREATE3) deploy, `Initializer::call` returns the typed +`initialize` call; `abi_encode` it into the proxy's init data. + Other entry points: - `deploy::upgrade_uups` — deploy a fresh implementation and @@ -85,6 +144,8 @@ Other entry points: and links external libraries before returning the creation bytecode. Nothing covered today links one; the UltraHonk verifiers the ceremony circuits bring will. +- `platform_verifier::codehash_at` — the code hash `setTrustRoots` wants + when a Platform Verifier is rotated onto a new circuit release. - `Artifacts::method_identifiers` — selector extraction from the vendored `methodIdentifiers`. diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index abe9c63..b25d9a7 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -8,7 +8,9 @@ //! is on neither `NotaryService` nor `CeremonyProofVerifier`: a contract on //! the route calls it with the fee attached, and the decoded claim comes back //! to that contract. What an operator does from here is initialize, rotate -//! the trust roots and move the governance parameters. +//! the trust roots and move the governance parameters — see +//! [`platform_verifier`](crate::platform_verifier) for the initializer that +//! checks the rules first. /// Bindings for `ceremony/NotaryService.sol` (which implements /// `INotaryService`). @@ -201,6 +203,9 @@ mod tls_notary_platform_verifier_inner { sol! { #[sol(rpc)] interface TlsNotaryPlatformVerifier { + /// Derives so the built call can be compared and printed by the + /// initializer that assembles it. + #[derive(Debug, PartialEq, Eq)] function initialize( address owner_, address notary_, @@ -292,6 +297,7 @@ mod google_platform_verifier_inner { sol! { #[sol(rpc)] interface GooglePlatformVerifier { + #[derive(Debug, PartialEq, Eq)] function initialize( address owner_, address notary_, diff --git a/rust/contracts/src/error.rs b/rust/contracts/src/error.rs index c1952ac..40b09a9 100644 --- a/rust/contracts/src/error.rs +++ b/rust/contracts/src/error.rs @@ -13,6 +13,13 @@ pub enum Error { /// What went wrong. detail: String, }, + /// A Platform Verifier initializer the contract would refuse, caught + /// before any transaction is sent. + #[error("initializer error: {detail}")] + Initializer { + /// Which rule, and which contract. + detail: String, + }, } /// Crate result alias. diff --git a/rust/contracts/src/lib.rs b/rust/contracts/src/lib.rs index 5b9884c..4503d15 100644 --- a/rust/contracts/src/lib.rs +++ b/rust/contracts/src/lib.rs @@ -22,6 +22,10 @@ //! cross-network factory address, install it (and the keyless CREATE2 //! deployer it hangs off) where missing, and deploy protocol proxies //! through it at name-derived CREATE3 addresses. +//! - [`platform_verifier`] — the Platform Verifier initializer: which +//! contract serves which platform, and an `initialize` call built with the +//! Honk verifier's code hash read off chain and the rules +//! `PlatformVerifierBase` enforces checked first. //! //! Signing is the consumer's concern: every helper takes a provider you have //! already wired with a wallet. @@ -31,6 +35,7 @@ pub mod bindings; pub mod deploy; mod error; pub mod factory; +pub mod platform_verifier; pub use artifacts::Artifacts; pub use error::{ diff --git a/rust/contracts/src/platform_verifier.rs b/rust/contracts/src/platform_verifier.rs new file mode 100644 index 0000000..701b698 --- /dev/null +++ b/rust/contracts/src/platform_verifier.rs @@ -0,0 +1,508 @@ +//! Deploying a launch Platform Verifier: which contract serves which +//! platform, what it initializes with, and the rules its `initialize` +//! enforces — checked here, off chain, before a transaction is built. +//! +//! `PlatformVerifierBase.__PlatformVerifierBase_init` refuses four things a +//! deployer would otherwise rediscover at the proxy's constructor revert: +//! a Notary Service that does not match what the profile notarizes (a +//! TLSNotary profile must hold one, Google must hold none), a code hash +//! that is zero, `keccak256("")` or not the hash of the code at the Honk +//! verifier's address, a parameter over its ceiling, and a zero owner or +//! root list. [`Initializer::call`] reads the code hash off the chain, checks +//! the rest, and builds the exact `initialize` call; +//! [`deploy_platform_verifier`] puts the implementation behind a fresh +//! ERC1967 proxy with it. +//! +//! The Honk verifier itself is not this crate's to deploy: it is +//! bb-generated from a `libid-circuits` release verification key, and a +//! Platform Verifier pins whichever one governance selected, by address AND +//! by code hash. + +use alloy::{ + primitives::{ + keccak256, + Address, + B256, + }, + providers::Provider, + sol_types::SolCall, +}; + +use crate::{ + artifacts::Artifacts, + bindings::ceremony::{ + GooglePlatformVerifier, + TlsNotaryPlatformVerifier, + }, + deploy::deploy_behind_proxy, + error::{ + Error, + Result, + }, +}; + +/// Ceiling on `proofLifetime`, in seconds: `PlatformVerifierBase.MAX_PROOF_LIFETIME`. +pub const MAX_PROOF_LIFETIME: u64 = 30 * 24 * 60 * 60; +/// Ceiling on `maxFutureAttestationSkew`, in seconds: +/// `PlatformVerifierBase.MAX_FUTURE_ATTESTATION_SKEW`. +pub const MAX_FUTURE_ATTESTATION_SKEW: u64 = 24 * 60 * 60; +/// Ceiling on `futureObservationAllowance`, in seconds: +/// `PlatformVerifierBase.MAX_FUTURE_OBSERVATION_ALLOWANCE`. +pub const MAX_FUTURE_OBSERVATION_ALLOWANCE: u64 = 24 * 60 * 60; + +/// One of the three launch Platform Verifiers. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PlatformVerifier { + /// `x/v1`: `XPlatformVerifier`, a TLSNotary profile. + X, + /// `github/v1`: `GitHubPlatformVerifier`, a TLSNotary profile. + GitHub, + /// `google/v1`: `GooglePlatformVerifier`, a signed-token profile that + /// notarizes nothing. + Google, +} + +impl PlatformVerifier { + /// Every launch verifier. + pub const ALL: [Self; 3] = [Self::X, Self::GitHub, Self::Google]; + + /// The contract, which is also its `.sol` file and its entry in + /// [`COVERED`](crate::artifacts::COVERED). + pub const fn contract(self) -> &'static str { + match self { + Self::X => "XPlatformVerifier", + Self::GitHub => "GitHubPlatformVerifier", + Self::Google => "GooglePlatformVerifier", + } + } + + /// The platform's bare name, as `CeremonyProfile` spells it. libID + /// namespaces only its own strings. + pub const fn platform(self) -> &'static str { + match self { + Self::X => "x", + Self::GitHub => "github", + Self::Google => "google", + } + } + + /// What the deployed contract answers to `platformId()`: `keccak256` + /// of the bare name. + pub fn platform_id(self) -> B256 { + keccak256(self.platform().as_bytes()) + } + + /// Whether the profile notarizes any session, and so whether its + /// verifier holds a Notary Service. `CeremonyProfile.attestationCount` + /// is two for the TLSNotary profiles and zero for Google; the + /// `libid-profiles` table says the same, and a test pins the two + /// together. + pub const fn notarizes(self) -> bool { + match self { + Self::X | Self::GitHub => true, + Self::Google => false, + } + } +} + +/// What a TLSNotary Platform Verifier (`x/v1`, `github/v1`) initializes +/// with: its trust roots and governance parameters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TlsNotaryRoots { + /// Governance. Rotates the roots, moves the parameters, upgrades. + pub owner: Address, + /// The Notary Service both attestations are authenticated through. + /// Required: the profile pins one (REQ-COMMON-18). + pub notary_service: Address, + /// The bb-generated UltraHonk verifier for this platform's circuit. Its + /// code hash is read off chain and pinned beside it. + pub honk_verifier: Address, + /// Maximum age of the token attestation, in seconds; at most + /// [`MAX_PROOF_LIFETIME`]. + pub proof_lifetime: u64, + /// Maximum lead over block time an attestation may carry, in seconds; + /// at most [`MAX_FUTURE_ATTESTATION_SKEW`]. + pub max_future_attestation_skew: u64, + /// How far ahead of block time the evidence time may run, in seconds; + /// at most [`MAX_FUTURE_OBSERVATION_ALLOWANCE`]. + pub future_observation_allowance: u64, +} + +/// What the Google Platform Verifier initializes with. No Notary Service: +/// the profile notarizes nothing, and the base refuses one. No lifetime and +/// no skew: the signed `exp` is the whole validity ceiling. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct GoogleRoots { + /// Governance. + pub owner: Address, + /// The bb-generated UltraHonk verifier for the Google OIDC circuit. + pub honk_verifier: Address, + /// How far ahead of block time the signed `exp` may run, in seconds; at + /// most [`MAX_FUTURE_OBSERVATION_ALLOWANCE`]. Google's runs about an + /// hour ahead. + pub future_observation_allowance: u64, + /// The `GoogleJwtRoots` proxy the trusted moduli are read through. + pub jwt_roots: Address, +} + +/// What one Platform Verifier is initialized with. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Initializer { + X(TlsNotaryRoots), + GitHub(TlsNotaryRoots), + Google(GoogleRoots), +} + +/// A built `initialize` call, typed by shape. Feed +/// [`abi_encode`](Self::abi_encode) to an ERC1967 proxy as its init data — +/// through [`deploy_platform_verifier`], [`deploy_proxy`](crate::deploy::deploy_proxy), +/// or as part of the creation code a [factory](crate::factory) deploy takes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InitializeCall { + /// `XPlatformVerifier.initialize` or `GitHubPlatformVerifier.initialize` + /// (one signature). + TlsNotary(TlsNotaryPlatformVerifier::initializeCall), + /// `GooglePlatformVerifier.initialize`. + Google(GooglePlatformVerifier::initializeCall), +} + +impl InitializeCall { + /// The ABI-encoded call. + pub fn abi_encode(&self) -> Vec { + match self { + Self::TlsNotary(call) => call.abi_encode(), + Self::Google(call) => call.abi_encode(), + } + } + + /// The code hash the call pins. + pub fn honk_verifier_codehash(&self) -> B256 { + match self { + Self::TlsNotary(call) => call.honkVerifierCodehash_, + Self::Google(call) => call.honkVerifierCodehash_, + } + } +} + +impl Initializer { + /// Which verifier this initializes. + pub const fn verifier(&self) -> PlatformVerifier { + match self { + Self::X(_) => PlatformVerifier::X, + Self::GitHub(_) => PlatformVerifier::GitHub, + Self::Google(_) => PlatformVerifier::Google, + } + } + + /// The Honk verifier it pins. + pub const fn honk_verifier(&self) -> Address { + match self { + Self::X(roots) | Self::GitHub(roots) => roots.honk_verifier, + Self::Google(roots) => roots.honk_verifier, + } + } + + /// The rules `initialize` enforces that need no chain: a nonzero owner, + /// a nonzero Honk verifier, a Notary Service where the profile notarizes + /// (the Google shape cannot carry one at all), a nonzero root list for + /// Google, and every parameter under its ceiling. The code hash is the + /// one rule left to [`call`](Self::call). + 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, + ) + } + } + } + + /// Build the `initialize` call: [`check`](Self::check), then read the + /// code hash of the Honk verifier through `provider` and pin it. Fails + /// when the address holds no code — the contract would refuse the + /// resulting hash, and a verifier that is not deployed yet is the + /// mis-wiring the check exists to catch. + pub async fn call(&self, provider: &P) -> Result { + 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, + // A profile whose Attestation Count is zero must not + // reach a Notary Service (REQ-COMMON-05D); the base + // refuses one. + notary_: Address::ZERO, + honkVerifier_: roots.honk_verifier, + honkVerifierCodehash_: codehash, + futureObservationAllowance_: roots.future_observation_allowance, + jwtRoots_: roots.jwt_roots, + }) + } + }) + } +} + +/// The code hash of the account at `address`, as `EXTCODEHASH` reports it +/// for an account with code: `keccak256` of its runtime bytecode. An +/// account without code is an error rather than `keccak256("")` or zero, +/// because `setTrustRoots` refuses both and a caller comparing against the +/// hash of nothing has nothing to pin. +pub async fn codehash_at(provider: &P, address: Address) -> Result { + 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)) +} + +/// Deploy the verifier's implementation from `artifacts` and put it behind +/// a fresh ERC1967 proxy initialized with `init` — the code hash read off +/// the chain, the rules checked first. Returns the proxy address, which is +/// the Platform Verifier a Proof Verifier registers with `setVerifier`. +/// +/// `sender` opts into explicit nonce management (see +/// [`deploy_contract_from`](crate::deploy::deploy_contract_from)). +pub async fn deploy_platform_verifier( + provider: &P, + artifacts: &Artifacts, + init: &Initializer, + sender: Option
, +) -> Result
{ + 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), + } + } + + /// Whether a verifier holds a Notary Service is derived from the + /// generated profile table, as the contract derives it from + /// `CeremonyProfile.attestationCount`. + #[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()); + } + + /// Every verifier's contract is one the crate vendors. + #[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}"); + } + // At the ceiling is allowed. + 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}"); + } + } +} From c3ace0af04bd462a94565e4f21d8a666d59b5a38 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Mon, 14 Sep 2026 16:52:06 +0100 Subject: [PATCH 3/3] test(contracts): deploy every Platform Verifier through the initializer On anvil, against the collaborators each pins. The views prove the initialization took, the Proof Verifier registers each one, the crate's ceilings match the contract's, and the wrapper's refusals are shown to be the contract's own reverts rather than rules of its own. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/tests/anvil.rs | 270 ++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index a115030..bcc1100 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -436,3 +436,273 @@ async fn deploys_the_ens_resolver_with_its_constructor_arguments() { .unwrap(); assert!(resolver.signers(next).call().await.unwrap()); } + +/// (e) The three Platform Verifiers through `deploy_platform_verifier`, on +/// the collaborators they pin: the Notary Service (the two TLSNotary ones), +/// the JWT root list (Google), and a Honk verifier — stood in for by any +/// contract with code, because `initialize` pins a code hash and never +/// calls `verify`. Each comes back initialized as the views say, registers +/// with the Proof Verifier, and the ceilings the crate restates are the +/// contract's. Then the rules: an initializer the wrapper refuses is one +/// the contract refuses too, and a Honk verifier with no code is caught +/// before any transaction. +#[tokio::test] +async fn deploys_and_initializes_every_platform_verifier() { + use alloy::{ + hex, + primitives::keccak256, + sol_types::SolError, + }; + use libid_contracts::{ + bindings::ceremony::{ + GooglePlatformVerifier, + TlsNotaryPlatformVerifier, + }, + platform_verifier::{ + codehash_at, + deploy_platform_verifier, + GoogleRoots, + Initializer, + PlatformVerifier, + TlsNotaryRoots, + MAX_FUTURE_ATTESTATION_SKEW, + MAX_FUTURE_OBSERVATION_ALLOWANCE, + MAX_PROOF_LIFETIME, + }, + Error, + }; + + let provider = test_provider(); + let artifacts = Artifacts::embedded(); + let deployer = default_signer(&provider).await; + let fee = U256::from(1_000); + + let notary_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "NotaryService", + &NotaryService::initializeCall { + owner_: deployer, + notary_: Address::repeat_byte(0x11), + fee_: fee, + }, + None, + ) + .await + .unwrap(); + let proof_verifier_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "CeremonyProofVerifier", + &CeremonyProofVerifier::initializeCall { owner_: deployer }, + None, + ) + .await + .unwrap(); + let roots_proxy = deploy_behind_proxy( + &provider, + &artifacts, + "GoogleJwtRoots", + &GoogleJwtRoots::initializeCall { + owner_: deployer, + notary_: notary_proxy, + }, + None, + ) + .await + .unwrap(); + let honk = deploy_contract( + &provider, + artifacts.bytecode("WTIA9").unwrap(), + "stand-in Honk verifier", + ) + .await + .unwrap(); + let honk_codehash = codehash_at(&provider, honk).await.unwrap(); + assert_ne!(honk_codehash, keccak256([])); + + let tls = TlsNotaryRoots { + owner: deployer, + notary_service: notary_proxy, + honk_verifier: honk, + proof_lifetime: libid_profiles::PROOF_LIFETIME_SECONDS_X, + max_future_attestation_skew: libid_profiles::MAX_FUTURE_ATTESTATION_SKEW_SECONDS, + future_observation_allowance: 300, + }; + let google = GoogleRoots { + owner: deployer, + honk_verifier: honk, + future_observation_allowance: 7200, + jwt_roots: roots_proxy, + }; + let proof_verifier = CeremonyProofVerifier::new(proof_verifier_proxy, &provider); + + for init in [ + Initializer::X(tls), + Initializer::GitHub(tls), + Initializer::Google(google), + ] { + let verifier = init.verifier(); + let proxy = deploy_platform_verifier(&provider, &artifacts, &init, None) + .await + .unwrap_or_else(|e| panic!("{verifier:?}: {e}")); + assert!(!provider.get_code_at(proxy).await.unwrap().is_empty()); + + // The quote is what the Proof Verifier forwards whole: one Notary + // Fee per attestation the profile requires. + let quote = match verifier { + PlatformVerifier::X | PlatformVerifier::GitHub => { + let v = TlsNotaryPlatformVerifier::new(proxy, &provider); + assert_eq!(v.owner().call().await.unwrap(), deployer); + assert_eq!(v.notaryService().call().await.unwrap(), notary_proxy); + assert_eq!(v.honkVerifier().call().await.unwrap(), honk); + assert_eq!( + v.honkVerifierCodehash().call().await.unwrap(), + honk_codehash + ); + let params = v.protocolParameters().call().await.unwrap(); + assert_eq!(params.proofLifetime, tls.proof_lifetime); + assert_eq!( + params.maxFutureAttestationSkew, + tls.max_future_attestation_skew + ); + assert_eq!( + params.futureObservationAllowance, + tls.future_observation_allowance + ); + assert_eq!( + v.MAX_PROOF_LIFETIME().call().await.unwrap(), + MAX_PROOF_LIFETIME + ); + assert_eq!( + v.MAX_FUTURE_ATTESTATION_SKEW().call().await.unwrap(), + MAX_FUTURE_ATTESTATION_SKEW + ); + assert_eq!( + v.MAX_FUTURE_OBSERVATION_ALLOWANCE().call().await.unwrap(), + MAX_FUTURE_OBSERVATION_ALLOWANCE + ); + let quote = v.quote().call().await.unwrap(); + assert_eq!(quote, fee * U256::from(2)); + quote + } + PlatformVerifier::Google => { + let v = GooglePlatformVerifier::new(proxy, &provider); + assert_eq!(v.owner().call().await.unwrap(), deployer); + assert_eq!(v.notaryService().call().await.unwrap(), Address::ZERO); + assert_eq!(v.honkVerifier().call().await.unwrap(), honk); + assert_eq!( + v.honkVerifierCodehash().call().await.unwrap(), + honk_codehash + ); + assert_eq!(v.jwtRoots().call().await.unwrap(), roots_proxy); + let params = v.protocolParameters().call().await.unwrap(); + assert_eq!(params.proofLifetime, 0); + assert_eq!(params.maxFutureAttestationSkew, 0); + assert_eq!( + params.futureObservationAllowance, + google.future_observation_allowance + ); + let quote = v.quote().call().await.unwrap(); + assert_eq!(quote, U256::ZERO); + quote + } + }; + + // The contract answers for the platform the crate says it serves, + // and the Proof Verifier registers it under that platform. + let platform_id = TlsNotaryPlatformVerifier::new(proxy, &provider) + .platformId() + .call() + .await + .unwrap(); + assert_eq!(platform_id, verifier.platform_id()); + proof_verifier + .setVerifier(platform_id, 1, proxy) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + assert_eq!( + proof_verifier + .verifierOf(platform_id, 1) + .call() + .await + .unwrap(), + proxy + ); + assert_eq!( + proof_verifier.quote(platform_id, 1).call().await.unwrap(), + quote + ); + } + + // A Honk verifier that is not deployed is caught before any transaction: + // the hash of nothing is exactly what the contract refuses to pin. + let err = Initializer::X(TlsNotaryRoots { + honk_verifier: Address::repeat_byte(0x99), + ..tls + }) + .call(&provider) + .await + .unwrap_err(); + assert!(matches!(err, Error::Initializer { .. }), "{err}"); + assert!(err.to_string().contains("no code at"), "{err}"); + + // The rules the wrapper enforces are the contract's, not its own: a + // hand-built Google initializer carrying a Notary Service, and an X one + // naming the wrong artifact, both revert at the proxy constructor with + // the error the wrapper's refusal names. Explicit nonces from here: + // a send that fails at gas estimation leaves alloy's cached nonce + // manager one ahead of the chain, and every later transaction would + // wait on a gap that never fills. + let google_with_notary = GooglePlatformVerifier::initializeCall { + owner_: deployer, + notary_: notary_proxy, + honkVerifier_: honk, + honkVerifierCodehash_: honk_codehash, + futureObservationAllowance_: 7200, + jwtRoots_: roots_proxy, + }; + let err = deploy_behind_proxy( + &provider, + &artifacts, + "GooglePlatformVerifier", + &google_with_notary, + Some(deployer), + ) + .await + .unwrap_err(); + assert!( + err.to_string().contains(&hex::encode( + GooglePlatformVerifier::WrongNotaryForProfile::SELECTOR + )), + "{err}" + ); + let x_wrong_artifact = TlsNotaryPlatformVerifier::initializeCall { + owner_: deployer, + notary_: notary_proxy, + honkVerifier_: honk, + honkVerifierCodehash_: keccak256("some other artifact"), + proofLifetime_: 3600, + maxFutureAttestationSkew_: 300, + futureObservationAllowance_: 300, + }; + let err = deploy_behind_proxy( + &provider, + &artifacts, + "XPlatformVerifier", + &x_wrong_artifact, + Some(deployer), + ) + .await + .unwrap_err(); + assert!( + err.to_string().contains(&hex::encode( + TlsNotaryPlatformVerifier::WrongVerifierArtifact::SELECTOR + )), + "{err}" + ); +}