From a839899827b651fd3828c9178f2844fb8b852a4e Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 02:38:38 +0100 Subject: [PATCH 01/15] feat(identity)!: IdentityJwksRoots reads a notarized session through NotaryService The Google JWKS trust list was the last consumer of the legacy Merkle transcript proof and the legacy `Notary` contract. Everything else on the verification path authenticates a section 9.1 record through `NotaryService` -- `verify(bytes attestedData, bytes proof)`, one fixed fee, the decoded record handed back only once a trusted key has vouched for it. The root list now does the same: a rotation is an ordinary notarized session, paid for like one, and `initialize` takes the Notary Service instead of `Notary`. What the keeper's prover produces is fixed by the design: everything in both directions revealed, nothing committed. That layout is what makes reading the transcript by concatenation safe -- the direction tiles the signed length, carries no commitment and starts at the origin, so the join IS the document that crossed the wire. A Platform Verifier reading around a commitment cannot make that claim, which is why this contract checks `HiddenBytes` where they check framing. The transcript checks are the contract's own, stated in the order they run: the origin-form request line (`GET /oauth2/v3/certs HTTP/1.1`), exactly one `Host` header counted over normalized bytes and equal to `www.googleapis.com` -- googleapis.com serves many virtual hosts under one certificate, so the TLS authority alone does not pin which backend answered -- the `HTTP/1.1 200 ` status line, the FIRST head boundary (a chunked body legitimately ends in `0\r\n\r\n`), and the body under whichever framing the head declares: strict chunked coding, an exact `Content-Length`, or `connection: close`. The body is read as the one document Google publishes: `keys` located by counting, flat objects, `kid` and `n` located the same way, and anything else refused by name. Kept verbatim: the monotonic per-kid rule that makes open replay harmless (an older reading is skipped, never reverted; an identical resubmission writes nothing and re-stamps nothing), the self-pruning cap, the base64url decoder and the 120-bit limb split -- the keeper's `decision::modulus_hash` and the Google circuit compute the same value, and a test pins the keeper's vector across both. Storage moves to ERC-7201 like every other contract here, with explicit getters standing in for the public variables. `CeremonyAttestation.normalizeHeaderBytes` is now `internal`: the `Host` count reads it, and what it strips decides what a count can miss, so the JWKS list shares the library's one implementation rather than carrying a second. Tests build the record with AttestationBuilder, sign with `vm.sign`, and run against a real NotaryService behind a proxy charging a real fee. Google's real body (fetched 2026-09-03) is a fixture, served chunked as Google serves it. Every revert is asserted by its exact selector, including the library's `CoverageGap` and NotaryService's `UntrustedNotary`. The legacy Merkle fixture and its fs permission go; UpgradeSafety pins the new namespace slot and initializes through the Notary Service; Deploy.s.sol passes the Notary Service it already deploys (a full rewrite of the script follows). Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .../ceremony/CeremonyAttestation.sol | 23 +- .../ceremony/test/UpgradeSafety.t.sol | 17 +- .../contracts/identity/IdentityJwksRoots.sol | 808 ++++++++--- .../identity/test/IdentityJwksRoots.t.sol | 1229 ++++++++++++----- .../identity/test/fixtures/google-jwks.json | 20 + .../test/fixtures/oidc/jwks-proof.json | 37 - solidity/foundry.toml | 9 +- solidity/script/Deploy.s.sol | 7 +- 8 files changed, 1499 insertions(+), 651 deletions(-) create mode 100644 solidity/contracts/identity/test/fixtures/google-jwks.json delete mode 100644 solidity/contracts/identity/test/fixtures/oidc/jwks-proof.json diff --git a/solidity/contracts/ceremony/CeremonyAttestation.sol b/solidity/contracts/ceremony/CeremonyAttestation.sol index 6c9d6eb1..b70e84a7 100644 --- a/solidity/contracts/ceremony/CeremonyAttestation.sol +++ b/solidity/contracts/ceremony/CeremonyAttestation.sol @@ -243,7 +243,7 @@ library CeremonyAttestation { // copy the whole revealed transcript again and rescan it, so every // rejected submission would pay twice for the check that rejected it -- // on a buffer the prover sizes. - uint256 headers = _countNeedle(_normalizeHeaderBytes(revealed)); + uint256 headers = _countNeedle(normalizeHeaderBytes(revealed)); if (headers != 1) revert NotOneAuthorizationHeader(headers); // Framing, on RAW bytes at known offsets. Two fixed comparisons make @@ -256,13 +256,20 @@ library CeremonyAttestation { } } - /// @dev Lowercase ASCII and drop every space and horizontal tab, keeping CR - /// and LF (REQ-COMMON-39). Field names and the scheme token are - /// case-insensitive and the colon admits whitespace, so a literal - /// search over raw bytes is evadable. Removing only bytes absent from - /// the needle can create a spurious match, which over-rejects and is - /// safe, but can never hide a real one. - function _normalizeHeaderBytes(bytes memory raw) private pure returns (bytes memory out) { + /// @notice Lowercase ASCII and drop every space and horizontal tab, keeping + /// CR and LF (REQ-COMMON-39). + /// + /// @dev Field names and the scheme token are case-insensitive and the + /// colon admits whitespace, so a literal search over raw bytes is + /// evadable. Removing only bytes absent from the needle can create a + /// spurious match, which over-rejects and is safe, but can never hide + /// a real one. + /// + /// `internal` for the same reason `concatRevealed` is: a header + /// COUNT reads this, and what it strips decides what a count can + /// miss -- so one implementation of it, shared with the JWKS root + /// list's `Host` scan, rather than two. + function normalizeHeaderBytes(bytes memory raw) internal pure returns (bytes memory out) { out = new bytes(raw.length); uint256 n; for (uint256 i = 0; i < raw.length; ++i) { diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index 9d70aced..cb17ab60 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -21,8 +21,6 @@ import {HandleVectors} from "../../identity/HandleVectors.sol"; import {HandleNormalizer} from "../../identity/HandleNormalizer.sol"; import {IdentityNodes} from "../../identity/IdentityNodes.sol"; import {StubPlatformVerifier} from "../../identity/test/StubPlatformVerifier.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; contract RHonk is IHonkVerifier { function verify(bytes calldata, bytes32[] calldata) external pure returns (bool) { @@ -67,6 +65,9 @@ contract UpgradeSafetyTest is Test { 0x92a7c997eeb454ceeeb8ef2d99ba7d4b830287ff966f129f9049c466a8342400 ); assertEq(_slot("libid.storage.IdentityNames"), NAMES_ROOT); + assertEq( + _slot("libid.storage.IdentityJwksRoots"), 0x1aceb787a5cb65251c62e0afbe6fbce480e0d1c9636ff7b90cdca2969527ef00 + ); } function _implOf(address proxy) internal view returns (address) { @@ -243,15 +244,15 @@ contract UpgradeSafetyTest is Test { // ─── IdentityJwksRoots ────────────────────────────────────────── function test_upgrade_IdentityJwksRoots() public { - Notary n = deployNotary(OWNER, NOTARY); + INotaryService ns = INotaryService(address(_notaryService())); IdentityJwksRoots impl = new IdentityJwksRoots(); vm.expectRevert(Initializable.InvalidInitialization.selector); - impl.initialize(OWNER, address(n)); + impl.initialize(OWNER, ns); IdentityJwksRoots r = IdentityJwksRoots( - address(new ERC1967Proxy(address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, address(n))))) + address(new ERC1967Proxy(address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, ns)))) ); vm.expectRevert(Initializable.InvalidInitialization.selector); - r.initialize(OWNER, address(n)); + r.initialize(OWNER, ns); IdentityJwksRoots impl2 = new IdentityJwksRoots(); _expectNotOwner(); @@ -259,7 +260,9 @@ contract UpgradeSafetyTest is Test { vm.prank(OWNER); r.upgradeToAndCall(address(impl2), ""); assertEq(_implOf(address(r)), address(impl2)); - assertEq(address(r.notaryContract()), address(n)); + assertEq(r.notaryService(), address(ns)); + assertEq(r.quoteRotation(), 7); + assertTrue(r.needsRotation()); assertEq(r.owner(), OWNER); } diff --git a/solidity/contracts/identity/IdentityJwksRoots.sol b/solidity/contracts/identity/IdentityJwksRoots.sol index afb2bd44..62963fc3 100644 --- a/solidity/contracts/identity/IdentityJwksRoots.sol +++ b/solidity/contracts/identity/IdentityJwksRoots.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {INotary} from "../notary/INotary.sol"; +import {CeremonyAttestation} from "../ceremony/CeremonyAttestation.sol"; +import {INotaryService} from "../ceremony/INotaryService.sol"; /// @title IdentityJwksRoots - which Google signing keys the naming system /// trusts, and until when. @@ -14,19 +15,30 @@ import {INotary} from "../notary/INotary.sol"; /// where the naming system learns the current ones, from a notarized /// reading of Google's published JWKS. /// -/// @dev **Rotation is permissionless, and deliberately has no nullifier.** With +/// @dev The reading is an ordinary notarized session -- the section 9.1 +/// record every Platform Verifier consumes, authenticated by the same +/// Notary Service and charged the same Notary Fee. What differs is the +/// layout the keeper's prover produces: everything in both directions is +/// revealed and nothing is committed. A public key set has nothing to +/// hide, and zero commitments is what lets this contract read the +/// transcript by concatenation safely -- with exact coverage and no +/// commitment, a cut between ranges cannot hide bytes. +/// +/// **Rotation is permissionless, and deliberately has no nullifier.** With /// an open caller set a one-shot nullifier would BE the attack: a -/// front-runner could consume a proof's digest and brick the honest -/// keeper. Freshness comes from the notary signature plus the window -/// below, and re-applying the same rotation is idempotent. +/// front-runner could consume a reading's digest and brick the honest +/// keeper. Freshness comes from the notary's signed `createdAt` plus the +/// window below, and re-applying the same reading is idempotent. A +/// rotation pays the Notary Fee, which is the one thing a submitter +/// needs beyond gas. /// -/// **The rotation proof names no contract.** Its digest is over the TLS -/// session alone — domain, randoms, transcript root, timestamp — so the -/// same notarized reading of Google's JWKS is valid anywhere the notary is -/// trusted. That cross-deployment replay is a feature, and the digest -/// shape must not grow a chain id or a verifying-contract binding. Which -/// key is trusted lives in the shared Notary contract; the digest itself -/// stays this contract's own. +/// **The reading names no contract.** The attested record is the TLS +/// session alone -- authority, notary clock, transcript -- so the same +/// notarized reading of Google's JWKS is valid anywhere the notary key is +/// trusted. That cross-deployment replay is a feature, and the record +/// must not grow a chain id or a verifying-contract binding. Which key is +/// trusted lives in the shared Notary Service; the transcript checks stay +/// this contract's own. /// /// **Why a contract of its own.** A verifier should verify. Rotation is a /// separate concern with a separate caller set and a separate key list, @@ -38,118 +50,222 @@ import {INotary} from "../notary/INotary.sol"; /// being trusted with no transaction from anyone. The owner's only lever /// over the key list is `untrustModulus`, which can only REMOVE trust. contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { - bytes32 private constant EXPECTED_DOMAIN_HASH = keccak256(bytes("www.googleapis.com")); - bytes private constant EXPECTED_DOMAIN = bytes("www.googleapis.com"); - bytes private constant EXPECTED_ENDPOINT = bytes("/oauth2/v3/certs"); + // ─── State ────────────────────────────────────────────────────── + + /// @custom:storage-location erc7201:libid.storage.IdentityJwksRoots + struct IdentityJwksRootsStorage { + /// Authenticates a reading and charges for it. + INotaryService notary; + /// kid keccak -> the limb-keccak the circuit produces. + mapping(bytes32 => bytes32) modulusOfKid; + mapping(bytes32 => uint256) expiresAtKid; + /// modulusHash -> expiry. This is what a verifier reads: the JWT + /// circuit does not expose `kid`, so trust has to be checkable from + /// the modulus alone. + mapping(bytes32 => uint256) trustedHashExpiresAt; + /// When each kid was last written, by the reading's own `createdAt`. + /// Rotation is open, so without this a replayed older reading inside + /// the freshness window would roll a kid back to a modulus Google has + /// retired and re-stamp it for another TTL. + mapping(bytes32 => uint256) rotatedAtKid; + /// Every kid currently tracked, so keepers can enumerate the list + /// without an indexer. Bounded by MAX_TRACKED_KIDS; expired entries + /// leave via `prune()`, which anyone may call. + bytes32[] trackedKids; + /// kid keccak -> index+1 in `trackedKids` (0 = not tracked). + mapping(bytes32 => uint256) trackedKidIndex; + /// The timestamp of the freshest reading ever applied -- the notary's + /// observation time, not the block it landed in. A keeper compares + /// this to wall-clock to decide whether its reading would be news. + uint256 freshestObservedAt; + } + + // keccak256(abi.encode(uint256(keccak256("libid.storage.IdentityJwksRoots")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant IDENTITY_JWKS_ROOTS_STORAGE = + 0x1aceb787a5cb65251c62e0afbe6fbce480e0d1c9636ff7b90cdca2969527ef00; + + function _s() private pure returns (IdentityJwksRootsStorage storage $) { + assembly { + $.slot := IDENTITY_JWKS_ROOTS_STORAGE + } + } + + // ─── Constants ────────────────────────────────────────────────── + + /// @notice The TLS server name the notary must have authenticated. + bytes32 public constant AUTHORITY = keccak256("www.googleapis.com"); uint256 public constant FRESHNESS_WINDOW = 1 hours; uint256 public constant CLOCK_SKEW_GRACE = 5 minutes; uint256 public constant DEFAULT_MODULUS_TTL = 30 days; /// The most distinct kids the list will track at once. Google publishes a /// handful of overlapping keys; a notarized reading cannot invent kids, so /// this cap is headroom, not a working limit. It exists so the enumeration - /// below stays a bounded loop no submitter — honest or not — can bloat. + /// below stays a bounded loop no submitter -- honest or not -- can bloat. uint256 public constant MAX_TRACKED_KIDS = 16; /// How much trusted runway `needsRotation()` insists on. Rotation re-stamps /// every key for DEFAULT_MODULUS_TTL, so a keeper acting on this signal /// rotates roughly weekly rather than racing the expiry. uint256 public constant RENEWAL_MARGIN = 7 days; - /// @notice The Notary contract a rotation's attestation is checked with. - INotary public notaryContract; - - /// @notice kid keccak -> the limb-keccak the circuit produces. - mapping(bytes32 => bytes32) public modulusOfKid; - mapping(bytes32 => uint256) public expiresAtKid; - - /// @notice modulusHash -> expiry. This is what a verifier reads: the JWT - /// circuit does not expose `kid`, so trust has to be checkable from - /// the modulus alone. - mapping(bytes32 => uint256) public trustedHashExpiresAt; - - /// @notice When each kid was last written, by the rotation proof's own - /// timestamp. Rotation is open, so without this a replayed older - /// proof inside the freshness window would roll a kid back to a - /// modulus Google has retired and re-stamp it for another TTL. - mapping(bytes32 => uint256) public rotatedAtKid; - - /// Every kid currently tracked, so keepers can enumerate the list without - /// an indexer. Bounded by MAX_TRACKED_KIDS; expired entries leave via - /// `prune()`, which anyone may call. - bytes32[] private _trackedKids; - /// kid keccak -> index+1 in `_trackedKids` (0 = not tracked). - mapping(bytes32 => uint256) private _trackedKidIndex; - /// The timestamp of the freshest reading ever applied — the notary's - /// observation time, not the block it landed in. A keeper compares this to - /// wall-clock to decide whether its reading would be news. - uint256 public freshestObservedAt; - - /// Reserved for future upgrades (UUPS). New state is appended before this - /// gap and the gap shrunk, so the layout survives. - uint256[47] private __gap; - + /// @dev The request line the keeper's prover puts on the wire, in + /// origin-form like every other verifier pins it. The path is what + /// separates the key set from everything else googleapis.com serves. + bytes private constant REQUEST_LINE = "GET /oauth2/v3/certs HTTP/1.1\r\n"; + /// @dev googleapis.com serves many virtual hosts under one certificate, so + /// the TLS authority alone does not pin which backend answered. The + /// `Host` header does, and the notary signed it. + bytes private constant HOST_NEEDLE = "\r\nhost:"; + bytes private constant EXPECTED_HOST = "www.googleapis.com"; + bytes private constant STATUS_LINE = "HTTP/1.1 200 "; + bytes private constant HEAD_BOUNDARY = "\r\n\r\n"; + bytes private constant CHUNKED_NEEDLE = "\r\ntransfer-encoding:chunked\r\n"; + bytes private constant CONTENT_LENGTH_NEEDLE = "\r\ncontent-length:"; + bytes private constant KEYS_TOKEN = '"keys"'; + + uint256 private constant MODULUS_BYTES = 256; + uint256 private constant MODULUS_LIMBS = 18; + + // ─── Events ───────────────────────────────────────────────────── + + event NotaryServiceChanged(address notary); event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); event ModulusUntrusted(bytes32 indexed modulusHash); /// One key written by a rotation, with the reading's own timestamp. What a /// keeper watches: `observedAt` orders readings, `expiresAt` says when this /// entry stops being trusted if no rotation lands before then. event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - /// An expired key left the tracked set. Permissionless — time retired the + /// An expired key left the tracked set. Permissionless -- time retired the /// key, `prune()` merely reclaimed the slot. event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); + // ─── Errors ───────────────────────────────────────────────────── + error ZeroAddress(); error TooManyKids(); - error UnknownNotary(); - error WrongDomain(); + /// @dev Exact value, both ways: the Notary Fee is forwarded whole, so there + /// is no overpayment to refund and nothing captured in transit. + error WrongValue(uint256 required, uint256 provided); + error WrongAuthority(bytes32 expected, bytes32 found); error FutureProof(); error StaleProof(); - error MerkleMismatch(); - error PathTooLong(); - error MissingKidInLeaf(); - error MissingNInLeaf(); + /// @dev A direction carries a commitment. The reading must hide nothing: + /// a hidden byte is a byte this contract reads around, and the join + /// below is only safe when there is nothing to read around. + error HiddenBytes(); + /// @dev The first revealed range of a direction does not begin the + /// transcript, so nothing says the bytes read as the request line (or + /// the status line) are it. Raised for either direction; a direction + /// revealing nothing reports `type(uint32).max`. + error RequestLineNotAtOrigin(uint32 start); + /// @dev Not `GET /oauth2/v3/certs HTTP/1.1\r\n`. + error WrongRequestLine(); + error NotOneHostHeader(uint256 count); + error WrongHost(); + /// @dev Not `HTTP/1.1 200 `. + error WrongStatusLine(); + error NoHeadBoundary(); + error BadChunkFraming(); + /// @dev The declared `Content-Length` is not the body the transcript + /// carries. A value that is not a decimal declares no length any + /// body can match. + error ContentLengthMismatch(uint256 declared, uint256 found); + /// @dev The member is absent, or present but not in the shape read here: + /// `keys` must be an array, `kid` and `n` must be strings. + error MissingMember(string name); + error AmbiguousMember(string name); + /// @dev A string value with no closing quote, or the `keys` array with no + /// closing bracket (or an element that is not an object). + error UnterminatedMember(string name); + /// @dev A backslash inside a value this contract reads. Neither a kid nor + /// a base64url modulus needs one, and unescaping is a parser this + /// contract does not carry. + error EscapedValue(string name); + /// @dev `{` or `[` inside a JWK object. + error UnexpectedNesting(); + /// @dev Bytes other than SP/HTAB/CR/LF after the closing `}`. + error TrailingBytes(); error InvalidModulusLength(); error InvalidB64Char(); error InvalidB64Length(); - /// One notarized reading of Google's JWKS endpoint. - struct NotarizedJwksProof { - bytes notarySignature; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] endpointPath; - } - - /// One key from that reading. - struct JwkClaim { - bytes jwkBytes; - bytes32[] jwkPath; - bytes kid; - bytes nB64url; - } - /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } - /// @param notaryContract_ The shared Notary contract (INotary). Notary key - /// rotation happens THERE; this contract holds only the pointer. - function initialize(address owner_, address notaryContract_) external initializer { + /// @param notary_ The shared Notary Service. Notary key rotation happens + /// THERE; this contract holds only the pointer. + function initialize(address owner_, INotaryService notary_) external initializer { __Ownable_init(owner_); __Ownable2Step_init(); __UUPSUpgradeable_init(); - if (notaryContract_ == address(0)) revert ZeroAddress(); - notaryContract = INotary(notaryContract_); + _setNotaryService(notary_); + } + + // ─── The Notary Service ───────────────────────────────────────── + + function notaryService() external view returns (address) { + return address(_s().notary); + } + + /// @notice Point the list at another Notary Service. + /// @dev The service is the trust root for every reading, so this is the + /// owner's one lever over what a rotation can install -- the same + /// lever every Platform Verifier's owner holds over its trust roots. + function setNotaryService(INotaryService notary_) external onlyOwner { + _setNotaryService(notary_); + } + + function _setNotaryService(INotaryService notary_) private { + if (address(notary_) == address(0)) revert ZeroAddress(); + _s().notary = notary_; + emit NotaryServiceChanged(address(notary_)); + } + + /// @notice What one rotation costs: the Notary Fee, forwarded whole. + function quoteRotation() external view returns (uint256) { + return _s().notary.fee(); } - /// @notice The current notary signer, read through the Notary contract. - function notary() external view returns (address) { - return notaryContract.notary(); + // ─── Rotation ─────────────────────────────────────────────────── + + /// @notice Apply a notarized reading of Google's JWKS. Anyone may call it. + /// The reading is the whole authorization: a keeper needs gas and + /// the Notary Fee, and nothing else -- no role, no allowlist, no + /// owner anywhere in the path. + /// + /// @param attestedData The exact bytes of ceremony-common section 9.1. + /// @param proof The notary's authentication of them, opaque here. + function rotate(bytes calldata attestedData, bytes calldata proof) external payable { + IdentityJwksRootsStorage storage $ = _s(); + + // Exact value, so there is no refund path to get wrong and no value + // can be captured in transit. + uint256 fee = $.notary.fee(); + if (msg.value != fee) revert WrongValue(fee, msg.value); + + // Authenticated and decoded in one call. The Notary Service owns the + // format its key vouches for; everything read below is a field it + // handed back, never a byte this contract decoded on its own. + CeremonyAttestation.AttestedData memory data = $.notary.verify{value: fee}(attestedData, proof); + + // The one thing in the record the notary observed rather than was + // told: the TLS server name it authenticated in the handshake. + if (data.authorityId != AUTHORITY) revert WrongAuthority(AUTHORITY, data.authorityId); + + uint256 createdAt = data.createdAt; + if (createdAt > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); + if (block.timestamp > createdAt + FRESHNESS_WINDOW) revert StaleProof(); + + _requireJwksRequest(data.sent, data.sentTranscriptLength); + bytes memory body = _jwksBody(data.received, data.recvTranscriptLength); + + // Monotonic high-water mark of reading timestamps. Replaying an older + // reading leaves it alone, so it only ever moves forward. + if (createdAt > $.freshestObservedAt) $.freshestObservedAt = createdAt; + + uint256 expiry = block.timestamp + DEFAULT_MODULUS_TTL; + _applyKeys(body, expiry, createdAt); } /// @notice Stop trusting a key before anything replaces it. @@ -165,21 +281,13 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// does with it is refuse bindings, which the rotation any keeper can /// submit undoes. function untrustModulus(bytes32 modulusHash) external onlyOwner { - delete trustedHashExpiresAt[modulusHash]; + delete _s().trustedHashExpiresAt[modulusHash]; emit ModulusUntrusted(modulusHash); } - /// @notice Apply a notarized reading of Google's JWKS. Anyone may call it. - /// The proof is the whole authorization: a keeper needs gas and - /// nothing else — no role, no allowlist, no owner anywhere in the - /// path. - function rotate(NotarizedJwksProof calldata proof, JwkClaim[] calldata claims) external { - _rotate(proof, claims); - } - /// @notice Drop every expired kid from the tracked set. Anyone may call it. /// - /// @dev Expiry itself needs no transaction — verifiers check the stamp at + /// @dev Expiry itself needs no transaction -- verifiers check the stamp at /// use-site, so an expired key is already untrusted the second its TTL /// passes. Pruning only reclaims enumeration slots (and the storage /// refund). Rotation calls this by itself when the set is full, so @@ -188,222 +296,482 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad _pruneExpired(); } - // ─── Keeper views ───────────────────────────────────────────── + // ─── Keeper views ─────────────────────────────────────────────── /// One tracked key, as a keeper sees it. struct RootInfo { bytes32 kidHash; bytes32 modulusHash; - /// The notarized reading that last wrote this kid (proof timestamp). + /// The notarized reading that last wrote this kid (its `createdAt`). uint256 observedAt; /// When this entry stops being trusted, absent a fresher rotation. uint256 expiresAt; } + function modulusOfKid(bytes32 kidHash) external view returns (bytes32) { + return _s().modulusOfKid[kidHash]; + } + + function expiresAtKid(bytes32 kidHash) external view returns (uint256) { + return _s().expiresAtKid[kidHash]; + } + + /// @notice modulusHash -> expiry. What `GooglePlatformVerifier` reads. + function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256) { + return _s().trustedHashExpiresAt[modulusHash]; + } + + function rotatedAtKid(bytes32 kidHash) external view returns (uint256) { + return _s().rotatedAtKid[kidHash]; + } + + function freshestObservedAt() external view returns (uint256) { + return _s().freshestObservedAt; + } + /// @notice Every tracked key with its provenance and expiry, expired ones /// included until someone prunes. One call tells a keeper the whole /// state of the list. function currentRoots() external view returns (RootInfo[] memory infos) { - uint256 n = _trackedKids.length; + IdentityJwksRootsStorage storage $ = _s(); + uint256 n = $.trackedKids.length; infos = new RootInfo[](n); - for (uint256 i = 0; i < n; i++) { - bytes32 kidHash = _trackedKids[i]; + for (uint256 i = 0; i < n; ++i) { + bytes32 kidHash = $.trackedKids[i]; infos[i] = RootInfo({ kidHash: kidHash, - modulusHash: modulusOfKid[kidHash], - observedAt: rotatedAtKid[kidHash], - expiresAt: expiresAtKid[kidHash] + modulusHash: $.modulusOfKid[kidHash], + observedAt: $.rotatedAtKid[kidHash], + expiresAt: $.expiresAtKid[kidHash] }); } } - /// @notice True when no key is guaranteed trusted RENEWAL_MARGIN from now — + /// @notice True when no key is guaranteed trusted RENEWAL_MARGIN from now -- /// the single bit a keeper polls to decide whether to fetch a fresh /// reading. True from deployment until the first rotation lands. function needsRotation() external view returns (bool) { + IdentityJwksRootsStorage storage $ = _s(); uint256 horizon = block.timestamp + RENEWAL_MARGIN; - uint256 n = _trackedKids.length; - for (uint256 i = 0; i < n; i++) { - bytes32 modulusHash = modulusOfKid[_trackedKids[i]]; - if (trustedHashExpiresAt[modulusHash] > horizon) return false; + uint256 n = $.trackedKids.length; + for (uint256 i = 0; i < n; ++i) { + bytes32 modulusHash = $.modulusOfKid[$.trackedKids[i]]; + if ($.trustedHashExpiresAt[modulusHash] > horizon) return false; } return true; } - function _rotate(NotarizedJwksProof memory p, JwkClaim[] memory claims) internal { - // The digest deliberately names no chain and no contract (see the - // contract comment); the Notary contract checks the attestation over - // it — EIP-191 + recover + signer compare today. - bytes32 digest = _notaryDigest(p); - if (!notaryContract.verify(digest, p.notarySignature)) revert UnknownNotary(); + // ─── The transcript ───────────────────────────────────────────── + + /// @dev The whole of one direction, joined. Three checks make the join + /// safe: the direction tiles the signed length, it hides nothing, and + /// its first range begins at the origin. Together they say every + /// transcript byte is in the result exactly once, at the offset the + /// notary signed it at -- so unlike a Platform Verifier reading around + /// a commitment, this contract may read the concatenation as the + /// document that crossed the wire. + function _wholeDirection(CeremonyAttestation.DirectionBlock memory block_, uint32 length) + private + pure + returns (bytes memory) + { + CeremonyAttestation.requireExactCoverage(block_, length); + if (block_.commitments.length != 0) revert HiddenBytes(); + // Unreachable once the two checks above pass with a nonempty + // transcript, and named for the empty one: no ranges pass coverage + // against a zero signed length, and an out-of-bounds panic would tell + // an operator nothing. + if (block_.revealed.length == 0) revert RequestLineNotAtOrigin(type(uint32).max); + if (block_.revealed[0].start != 0) revert RequestLineNotAtOrigin(block_.revealed[0].start); + return CeremonyAttestation.concatRevealed(block_); + } - if (p.timestamp > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); - if (block.timestamp > p.timestamp + FRESHNESS_WINDOW) revert StaleProof(); + /// @dev The request must be the keeper's, to Google's JWKS endpoint, on + /// Google's own host. + function _requireJwksRequest(CeremonyAttestation.DirectionBlock memory block_, uint32 length) private pure { + bytes memory sent = _wholeDirection(block_, length); + if (!_startsWith(sent, REQUEST_LINE)) revert WrongRequestLine(); + + // Counted over normalized bytes, so a second `Host` cannot hide + // behind case or whitespace (REQ-COMMON-39's rule, applied to a + // different header). Exactly one: with two, which the backend + // honoured is the backend's business, not something this contract + // can know. + bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(sent); + (uint256 count, uint256 at) = _find(normalized, HOST_NEEDLE); + if (count != 1) revert NotOneHostHeader(count); + + uint256 from = at + HOST_NEEDLE.length; + uint256 to = _indexOf(normalized, "\r\n", from); + if (to == type(uint256).max || to - from != EXPECTED_HOST.length) revert WrongHost(); + for (uint256 i = 0; i < EXPECTED_HOST.length; ++i) { + if (normalized[from + i] != EXPECTED_HOST[i]) revert WrongHost(); + } + } - if (p.domainHash != EXPECTED_DOMAIN_HASH) revert WrongDomain(); - _verifyLeaf(p.domainPath, p.transcriptRoot, "domain:", EXPECTED_DOMAIN); - _verifyLeaf(p.endpointPath, p.transcriptRoot, "endpoint:", EXPECTED_ENDPOINT); + /// @dev The response body, located by the framing the server itself + /// wrote: the head boundary first, then whichever of chunked or + /// `Content-Length` the head declares. + /// + /// The FIRST head boundary, not the only one. A chunked body ends in + /// `0\r\n\r\n`, so uniqueness -- which the token request demands -- + /// would refuse every real Google response. + function _jwksBody(CeremonyAttestation.DirectionBlock memory block_, uint32 length) + private + pure + returns (bytes memory body) + { + bytes memory received = _wholeDirection(block_, length); + if (!_startsWith(received, STATUS_LINE)) revert WrongStatusLine(); + + uint256 boundary = _indexOf(received, HEAD_BOUNDARY, 0); + if (boundary == type(uint256).max) revert NoHeadBoundary(); + // The head keeps the CRLF that closes its last line, so a line-anchored + // needle matches that line too. + bytes memory head = _slice(received, 0, boundary + 2); + body = _slice(received, boundary + 4, received.length); + + bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(head); + (uint256 chunked,) = _find(normalized, CHUNKED_NEEDLE); + if (chunked != 0) return _dechunk(body); + + (uint256 declaredLength, uint256 at) = _find(normalized, CONTENT_LENGTH_NEEDLE); + if (declaredLength != 0) { + uint256 declared = _decimal(normalized, at + CONTENT_LENGTH_NEEDLE.length, body.length); + if (declared != body.length) revert ContentLengthMismatch(declared, body.length); + } + // Neither: `connection: close` delimits, and the body is what follows + // the head. + } - // Monotonic high-water mark of reading timestamps. Replaying an older - // proof leaves it alone, so it only ever moves forward. - if (p.timestamp > freshestObservedAt) freshestObservedAt = p.timestamp; + /// @dev A decimal run ending at CRLF. Anything else is a length no body + /// can match, and is reported as such rather than parsed leniently. + function _decimal(bytes memory data, uint256 at, uint256 found) private pure returns (uint256 value) { + uint256 digits; + for (; at < data.length; ++at) { + bytes1 c = data[at]; + if (c == 0x0d) break; + if (c < 0x30 || c > 0x39) revert ContentLengthMismatch(value, found); + // A transcript length is a u32; ten digits already exceed it. + if (++digits > 10) revert ContentLengthMismatch(value, found); + value = value * 10 + (uint8(c) - 0x30); + } + } - uint256 expiry = block.timestamp + DEFAULT_MODULUS_TTL; - for (uint256 i = 0; i < claims.length; i++) { - _processClaim(claims[i], p.transcriptRoot, expiry, p.timestamp); + /// @dev Strict chunked transfer coding: a hex size of one to eight digits, + /// CRLF, that many bytes, CRLF; a zero size ends it and must be + /// followed by CRLF and nothing more. No extensions, no trailers. + /// Every iteration consumes at least five bytes, so the loop is + /// bounded by the body. + function _dechunk(bytes memory chunked) private pure returns (bytes memory out) { + out = new bytes(chunked.length); + uint256 n; + uint256 at; + while (true) { + uint256 size; + uint256 digits; + while (at < chunked.length) { + uint256 nibble = _hexNibble(chunked[at]); + if (nibble == type(uint256).max) break; + if (++digits > 8) revert BadChunkFraming(); + size = (size << 4) | nibble; + ++at; + } + // Covers a missing size, a chunk extension (`;`) and any other + // byte where CRLF belongs. + if (digits == 0 || !_crlfAt(chunked, at)) revert BadChunkFraming(); + at += 2; + + if (size == 0) { + if (chunked.length - at != 2 || !_crlfAt(chunked, at)) revert BadChunkFraming(); + break; + } + if (at + size + 2 > chunked.length) revert BadChunkFraming(); + for (uint256 i = 0; i < size; ++i) { + out[n++] = chunked[at + i]; + } + at += size; + if (!_crlfAt(chunked, at)) revert BadChunkFraming(); + at += 2; + } + assembly ("memory-safe") { + mstore(out, n) + } + } + + // ─── The key set ──────────────────────────────────────────────── + + /// @dev Read every JWK out of the body and apply it. The body is Google's + /// pretty-printed JSON; this reads exactly the shape Google publishes + /// and refuses anything it does not recognise, so it is a checker + /// for one document rather than a JSON parser. + /// + /// `keys` is located by counting: the token appears once in the + /// whole body, or the document is not the one this reads. Objects are + /// flat -- a `{` or `[` inside one is refused -- so each ends at its + /// first `}` and the walk is one forward pass bounded by the body, + /// with the object count capped at MAX_TRACKED_KIDS. + function _applyKeys(bytes memory body, uint256 expiry, uint256 provenAt) private { + (uint256 count, uint256 at) = _find(body, KEYS_TOKEN); + if (count == 0) revert MissingMember("keys"); + if (count > 1) revert AmbiguousMember("keys"); + + at = _ws(body, at + KEYS_TOKEN.length); + if (at >= body.length || body[at] != ":") revert MissingMember("keys"); + at = _ws(body, at + 1); + if (at >= body.length || body[at] != "[") revert MissingMember("keys"); + ++at; + + uint256 keys; + while (true) { + at = _ws(body, at); + if (at >= body.length) revert UnterminatedMember("keys"); + if (body[at] == "]") { + ++at; + break; + } + if (keys != 0) { + if (body[at] != ",") revert UnterminatedMember("keys"); + at = _ws(body, at + 1); + if (at >= body.length) revert UnterminatedMember("keys"); + } + if (body[at] != "{") revert UnterminatedMember("keys"); + if (keys == MAX_TRACKED_KIDS) revert TooManyKids(); + + uint256 close = _objectEnd(body, at); + _applyKey(_slice(body, at, close + 1), expiry, provenAt); + ++keys; + at = close + 1; + } + + at = _ws(body, at); + if (at >= body.length || body[at] != "}") revert TrailingBytes(); + if (_ws(body, at + 1) != body.length) revert TrailingBytes(); + } + + /// @dev The index of the `}` closing the object opened at `open`. + function _objectEnd(bytes memory body, uint256 open) private pure returns (uint256 at) { + for (at = open + 1; at < body.length; ++at) { + bytes1 c = body[at]; + if (c == "}") return at; + if (c == "{" || c == "[") revert UnexpectedNesting(); } + revert UnterminatedMember("keys"); } - function _processClaim(JwkClaim memory c, bytes32 transcriptRoot, uint256 expiry, uint256 provenAt) internal { - if (c.jwkPath.length > 32) revert PathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", c.jwkBytes)))); - if (!_merkleVerifyMem(c.jwkPath, transcriptRoot, leaf)) revert MerkleMismatch(); + function _applyKey(bytes memory jwk, uint256 expiry, uint256 provenAt) private { + bytes memory kid = _member(jwk, "kid"); + bytes memory nB64url = _member(jwk, "n"); - if (!_containsKeyValue(c.jwkBytes, "kid", c.kid)) revert MissingKidInLeaf(); - if (!_containsKeyValue(c.jwkBytes, "n", c.nB64url)) revert MissingNInLeaf(); + bytes memory rawN = _b64urlDecode(nB64url); + if (rawN.length != MODULUS_BYTES) revert InvalidModulusLength(); + _processClaim(kid, _modulusHash(rawN), expiry, provenAt); + } - bytes memory rawN = _b64urlDecode(c.nB64url); - if (rawN.length != 256) revert InvalidModulusLength(); - bytes32[18] memory limbs = _splitLimbs(rawN); + /// @dev One string member of a flat object. The token `"name"` appears + /// exactly once, then `:` and an opening quote with whitespace either + /// side (Google writes a space after the colon), and the value runs + /// to the next quote. A backslash is refused rather than unescaped. + function _member(bytes memory jwk, string memory name) private pure returns (bytes memory) { + bytes memory token = abi.encodePacked('"', name, '"'); + (uint256 count, uint256 at) = _find(jwk, token); + if (count == 0) revert MissingMember(name); + if (count > 1) revert AmbiguousMember(name); + + at = _ws(jwk, at + token.length); + if (at >= jwk.length || jwk[at] != ":") revert MissingMember(name); + at = _ws(jwk, at + 1); + if (at >= jwk.length || jwk[at] != '"') revert MissingMember(name); + ++at; + + uint256 start = at; + for (; at < jwk.length; ++at) { + bytes1 c = jwk[at]; + if (c == '"') return _slice(jwk, start, at); + if (c == "\\") revert EscapedValue(name); + } + revert UnterminatedMember(name); + } - bytes memory packed = new bytes(18 * 32); - for (uint256 i = 0; i < 18; i++) { + /// @dev The hash the Google circuit exposes: keccak over the eighteen + /// 120-bit limbs as 32-byte words. The keeper's `decision::modulus_hash` + /// computes the same value, and a test pins one vector across both. + function _modulusHash(bytes memory rawN) private pure returns (bytes32) { + bytes32[18] memory limbs = _splitLimbs(rawN); + bytes memory packed = new bytes(MODULUS_LIMBS * 32); + for (uint256 i = 0; i < MODULUS_LIMBS; ++i) { bytes32 v = limbs[i]; assembly { mstore(add(add(packed, 32), mul(i, 32)), v) } } - bytes32 modulusHash = keccak256(packed); + return keccak256(packed); + } - bytes32 kidHash = keccak256(c.kid); - // Ignore a claim proved no later than the one already applied — the + function _processClaim(bytes memory kid, bytes32 modulusHash, uint256 expiry, uint256 provenAt) private { + IdentityJwksRootsStorage storage $ = _s(); + bytes32 kidHash = keccak256(kid); + // Ignore a claim proved no later than the one already applied -- the // monotonic rule that makes replay harmless. Rotation is open and a - // proof binds no contract, so anyone can replay any still-fresh + // reading binds no contract, so anyone can replay any still-fresh // reading anywhere, forever; ignoring (never reverting) keeps a // front-runner from bricking the honest keeper's batch by landing one // claim from it first, and skipping (never applying) keeps an older // reading from rolling a kid back or re-stamping its TTL. - if (provenAt < rotatedAtKid[kidHash]) return; - bytes32 previous = modulusOfKid[kidHash]; + if (provenAt < $.rotatedAtKid[kidHash]) return; + bytes32 previous = $.modulusOfKid[kidHash]; // A byte-identical resubmission of the applied reading is a no-op: // nothing is written, nothing is emitted, and in particular the TTL is - // NOT re-stamped — so spamming the same proof neither grows state nor - // stretches trust. - if (provenAt == rotatedAtKid[kidHash] && previous == modulusHash) return; - rotatedAtKid[kidHash] = provenAt; + // NOT re-stamped -- so spamming the same reading neither grows state + // nor stretches trust. + if (provenAt == $.rotatedAtKid[kidHash] && previous == modulusHash) return; + $.rotatedAtKid[kidHash] = provenAt; // The key this kid used to carry stops being trusted NOW, rather than // when its thirty-day stamp runs out. // // The verifier resolves by modulus, not by kid, so leaving the old - // entry keeps a retired key usable for the rest of its TTL — which is + // entry keeps a retired key usable for the rest of its TTL -- which is // exactly the window a compromised key would be used in. Google // publishes overlapping keys and tokens live about an hour, so the cost // is that a token signed by the key Google just retired stops verifying // a little early; the user signs in again. if (previous != bytes32(0) && previous != modulusHash) { - delete trustedHashExpiresAt[previous]; + delete $.trustedHashExpiresAt[previous]; emit ModulusUntrusted(previous); } else if (previous == bytes32(0)) { _trackKid(kidHash); } - modulusOfKid[kidHash] = modulusHash; - expiresAtKid[kidHash] = expiry; - trustedHashExpiresAt[modulusHash] = expiry; - emit ModulusRotated(kidHash, string(c.kid), modulusHash, expiry); + $.modulusOfKid[kidHash] = modulusHash; + $.expiresAtKid[kidHash] = expiry; + $.trustedHashExpiresAt[modulusHash] = expiry; + emit ModulusRotated(kidHash, string(kid), modulusHash, expiry); emit RootApplied(kidHash, modulusHash, provenAt, expiry); } /// Add a kid to the enumeration. Growth is doubly bounded: a kid can only /// enter through a notarized reading of Google's own JWKS (a submitter - /// cannot invent one), and the set is capped — when full, expired entries + /// cannot invent one), and the set is capped -- when full, expired entries /// are pruned to make room, and only a set full of LIVE keys refuses. - function _trackKid(bytes32 kidHash) internal { - if (_trackedKidIndex[kidHash] != 0) return; - if (_trackedKids.length >= MAX_TRACKED_KIDS) { + function _trackKid(bytes32 kidHash) private { + IdentityJwksRootsStorage storage $ = _s(); + if ($.trackedKidIndex[kidHash] != 0) return; + if ($.trackedKids.length >= MAX_TRACKED_KIDS) { _pruneExpired(); - if (_trackedKids.length >= MAX_TRACKED_KIDS) revert TooManyKids(); + if ($.trackedKids.length >= MAX_TRACKED_KIDS) revert TooManyKids(); } - _trackedKids.push(kidHash); - _trackedKidIndex[kidHash] = _trackedKids.length; + $.trackedKids.push(kidHash); + $.trackedKidIndex[kidHash] = $.trackedKids.length; } /// Swap-remove every expired kid. `rotatedAtKid` survives on purpose: it is /// the monotonic floor that keeps a replayed old reading from resurrecting /// the entry the prune just removed. - function _pruneExpired() internal { - uint256 i = _trackedKids.length; + function _pruneExpired() private { + IdentityJwksRootsStorage storage $ = _s(); + uint256 i = $.trackedKids.length; while (i > 0) { i--; - bytes32 kidHash = _trackedKids[i]; - if (expiresAtKid[kidHash] > block.timestamp) continue; + bytes32 kidHash = $.trackedKids[i]; + if ($.expiresAtKid[kidHash] > block.timestamp) continue; - bytes32 modulusHash = modulusOfKid[kidHash]; - delete modulusOfKid[kidHash]; - delete expiresAtKid[kidHash]; - // Only clear the verifier-facing stamp if it is genuinely spent — + bytes32 modulusHash = $.modulusOfKid[kidHash]; + delete $.modulusOfKid[kidHash]; + delete $.expiresAtKid[kidHash]; + // Only clear the verifier-facing stamp if it is genuinely spent -- // it can sit above the kid's own expiry only if some fresher // rotation re-trusted the same modulus under another kid. - if (trustedHashExpiresAt[modulusHash] <= block.timestamp) { - delete trustedHashExpiresAt[modulusHash]; + if ($.trustedHashExpiresAt[modulusHash] <= block.timestamp) { + delete $.trustedHashExpiresAt[modulusHash]; } - uint256 last = _trackedKids.length - 1; + uint256 last = $.trackedKids.length - 1; if (i != last) { - bytes32 moved = _trackedKids[last]; - _trackedKids[i] = moved; - _trackedKidIndex[moved] = i + 1; + bytes32 moved = $.trackedKids[last]; + $.trackedKids[i] = moved; + $.trackedKidIndex[moved] = i + 1; } - _trackedKids.pop(); - delete _trackedKidIndex[kidHash]; + $.trackedKids.pop(); + delete $.trackedKidIndex[kidHash]; emit RootPruned(kidHash, modulusHash); } } - function _verifyLeaf(bytes32[] memory path, bytes32 root, string memory prefix, bytes memory value) internal pure { - if (path.length > 32) revert PathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - if (!_merkleVerifyMem(path, root, leaf)) revert MerkleMismatch(); - } + // ─── Bytes ────────────────────────────────────────────────────── - function _merkleVerifyMem(bytes32[] memory path, bytes32 root, bytes32 leaf) internal pure returns (bool) { - bytes32 cur = leaf; - for (uint256 i = 0; i < path.length; i++) { - cur = _hashPair(cur, path[i]); + /// @dev How many times `needle` occurs in `haystack`, and where the first + /// occurrence begins. Overlapping occurrences count, which can only + /// over-count -- the direction that fails closed. + function _find(bytes memory haystack, bytes memory needle) private pure returns (uint256 count, uint256 first) { + first = type(uint256).max; + for (uint256 i = 0; i + needle.length <= haystack.length; ++i) { + bool hit = true; + for (uint256 j = 0; j < needle.length; ++j) { + if (haystack[i + j] != needle[j]) { + hit = false; + break; + } + } + if (hit) { + if (count == 0) first = i; + ++count; + } } - return cur == root; } - function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - function _containsKeyValue(bytes memory haystack, bytes memory key, bytes memory value) - internal - pure - returns (bool) - { - return _contains(haystack, abi.encodePacked('"', key, '":"', value, '"')) - || _contains(haystack, abi.encodePacked('"', key, '": "', value, '"')); - } - - function _contains(bytes memory haystack, bytes memory needle) internal pure returns (bool) { - uint256 hLen = haystack.length; - uint256 nLen = needle.length; - if (nLen == 0) return true; - if (nLen > hLen) return false; - for (uint256 i = 0; i + nLen <= hLen; i++) { - bool m = true; - for (uint256 j = 0; j < nLen; j++) { + /// @dev The first occurrence of `needle` at or after `from`, or + /// `type(uint256).max`. + function _indexOf(bytes memory haystack, bytes memory needle, uint256 from) private pure returns (uint256) { + for (uint256 i = from; i + needle.length <= haystack.length; ++i) { + bool hit = true; + for (uint256 j = 0; j < needle.length; ++j) { if (haystack[i + j] != needle[j]) { - m = false; + hit = false; break; } } - if (m) return true; + if (hit) return i; } - return false; + return type(uint256).max; + } + + function _startsWith(bytes memory data, bytes memory prefix) private pure returns (bool) { + if (data.length < prefix.length) return false; + for (uint256 i = 0; i < prefix.length; ++i) { + if (data[i] != prefix[i]) return false; + } + return true; + } + + function _slice(bytes memory data, uint256 from, uint256 to) private pure returns (bytes memory out) { + out = new bytes(to - from); + for (uint256 i = 0; i < out.length; ++i) { + out[i] = data[from + i]; + } + } + + /// @dev Skip JSON whitespace: SP, HTAB, CR, LF. + function _ws(bytes memory data, uint256 at) private pure returns (uint256) { + while (at < data.length) { + bytes1 c = data[at]; + if (c != 0x20 && c != 0x09 && c != 0x0d && c != 0x0a) break; + ++at; + } + return at; + } + + function _crlfAt(bytes memory data, uint256 at) private pure returns (bool) { + return at + 2 <= data.length && data[at] == 0x0d && data[at + 1] == 0x0a; + } + + /// @dev The value of a hex digit, or `type(uint256).max` for anything else. + function _hexNibble(bytes1 c) private pure returns (uint256) { + uint8 v = uint8(c); + if (v >= 0x30 && v <= 0x39) return v - 0x30; + if (v >= 0x41 && v <= 0x46) return v - 0x41 + 10; + if (v >= 0x61 && v <= 0x66) return v - 0x61 + 10; + return type(uint256).max; } function _b64urlDecode(bytes memory input) internal pure returns (bytes memory) { @@ -451,6 +819,9 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad revert InvalidB64Char(); } + /// @dev Eighteen little-endian 120-bit limbs of a 2048-bit big-endian + /// modulus, the way the Google circuit takes its public inputs. Kept + /// verbatim: the keeper and the circuit compute the same split. function _splitLimbs(bytes memory be) internal pure returns (bytes32[18] memory limbs) { uint256[8] memory words; assembly { @@ -485,22 +856,11 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad } } - function _notaryDigest(NotarizedJwksProof memory p) internal pure returns (bytes32) { - return keccak256( - abi.encode( - p.domainHash, - p.clientRandom, - p.serverRandom, - keccak256(p.serverEphemeralKey), - p.transcriptRoot, - p.timestamp - ) - ); - } + /// @dev Required by UUPS -- only the owner can upgrade. function _authorizeUpgrade(address) internal override onlyOwner {} - /// @dev Renouncing would freeze the notary allowlist, and Google's keys - /// would expire with no way to replace them. + /// @dev Renouncing would freeze the Notary Service pointer, and Google's + /// keys would expire with no way to replace them. function renounceOwnership() public pure override { revert("renounce disabled"); } diff --git a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol index 09af364c..13c1dd8f 100644 --- a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol +++ b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol @@ -1,383 +1,362 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; +import {Test, Vm} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {AttestationBuilder} from "../../ceremony/test/AttestationBuilder.sol"; +import {CeremonyAttestation} from "../../ceremony/CeremonyAttestation.sol"; +import {INotaryService} from "../../ceremony/INotaryService.sol"; +import {NotaryService} from "../../ceremony/NotaryService.sol"; import {IdentityJwksRoots} from "../IdentityJwksRoots.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// The naming system's Google trust list, fed by a notarized reading of /// Google's JWKS endpoint. /// -/// The fixture is a real notarized rotation, signed by the deterministic test -/// notary. It is this tree's own copy on purpose: the proof binds no contract, -/// so the same reading is valid wherever that notary is trusted — but reading -/// it out of another tree would be the coupling this system exists without. +/// The reading is an ordinary section 9.1 session: built with the same +/// builder every Platform Verifier test uses, signed at test time by the anvil +/// key, and authenticated by a real NotaryService behind a proxy charging a +/// real fee. Nothing here mocks the thing under test. Google's real body rides +/// along as a fixture, so the parser is tested against the document it will +/// read rather than a shape a test author remembered. contract IdentityJwksRootsTest is Test { - IdentityJwksRoots internal roots; - Notary internal notaryContract; - - address internal constant NOTARY = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; - /// A notary this test can sign as, for the cases the fixture cannot reach. - uint256 internal constant TEST_NOTARY_PK = 0xA11CE; - bytes32 internal constant EXPECTED_DOMAIN_HASH = keccak256(bytes("www.googleapis.com")); - - address internal owner = makeAddr("owner"); - address internal keeper = makeAddr("keeper"); - - IdentityJwksRoots.NotarizedJwksProof internal proof; - IdentityJwksRoots.JwkClaim[] internal claims; - uint256 internal provenAt; + IdentityJwksRoots roots; + NotaryService notary; + + address constant OWNER = address(0xA11CE); + address keeper = makeAddr("keeper"); + /// The anvil key. + uint256 constant NOTARY_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + uint256 constant FEE = 0.001 ether; + uint64 constant T0 = 1_770_000_000; + bytes32 constant AUTHORITY = keccak256("www.googleapis.com"); + + /// Google's real body, fetched 2026-09-03: pretty-printed, two keys. + string constant GOOGLE_JWKS = "contracts/identity/test/fixtures/google-jwks.json"; + bytes32 constant GOOGLE_KID_1 = keccak256("943a3a5d7d919625a454e489b75c29adab57acba"); + bytes32 constant GOOGLE_KID_2 = keccak256("f10f87405a979c1df36df26606734f33cd85c271"); + + /// The keeper's limb-hash vector. `decision::modulus_hash` in the keeper + /// and `_splitLimbs` here must agree on this one, or a key the keeper + /// reports as trusted is one the verifier refuses. + string constant KEEPER_MODULUS = + "BAsSGSAnLjU8Q0pRWF9mbXR7gomQl56lrLO6wcjP1t3k6_L5BQwTGiEoLzY9REtSWWBnbnV8g4qRmJ-mrbS7wsnQ197l7PP6Bg0UGyIpMDc-RUxTWmFob3Z9hIuSmaCnrrW8w8rR2N_m7fT7Bw4VHCMqMTg_Rk1UW2JpcHd-hYyTmqGor7a9xMvS2eDn7vUBCA8WHSQrMjlAR05VXGNqcXh_ho2Um6KpsLe-xczT2uHo7_YCCRAXHiUsMzpBSE9WXWRrcnmAh46VnKOqsbi_xs3U2-Lp8PcDChEYHyYtNDtCSVBXXmVsc3qBiI-WnaSrsrnAx87V3OPq8fgECxIZIA"; + bytes32 constant KEEPER_MODULUS_HASH = 0xc5c1457ba4adf8bb84324e8595bf883639aa7ebd7aa7e7626a0b51f90e6838bc; + + /// The request the keeper's prover sends, byte for byte. + bytes constant REQUEST = "GET /oauth2/v3/certs HTTP/1.1\r\n" "host: www.googleapis.com\r\n" "connection: close\r\n" + "accept: application/json\r\n" "user-agent: libid-keeper/0.1.0\r\n" "\r\n"; + bytes constant STATUS_OK = "HTTP/1.1 200 OK\r\n"; + bytes constant CONTENT_TYPE = "content-type: application/json; charset=UTF-8\r\n"; function setUp() public { - notaryContract = deployNotary(owner, NOTARY); - IdentityJwksRoots impl = new IdentityJwksRoots(); - roots = IdentityJwksRoots( + vm.warp(T0 + 10); + + NotaryService nImpl = new NotaryService(); + notary = NotaryService( address( new ERC1967Proxy( - address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (owner, address(notaryContract))) + address(nImpl), abi.encodeCall(NotaryService.initialize, (OWNER, vm.addr(NOTARY_KEY), FEE)) ) ) ); + roots = _deployRoots(INotaryService(address(notary))); - _loadFixture(); - vm.warp(provenAt + 60); - } - - function _loadFixture() internal { - string memory j = vm.readFile("contracts/identity/test/fixtures/oidc/jwks-proof.json"); - proof.notarySignature = vm.parseJsonBytes(j, ".notary_signature"); - proof.domainHash = vm.parseJsonBytes32(j, ".domain_hash"); - proof.clientRandom = vm.parseJsonBytes32(j, ".client_random"); - proof.serverRandom = vm.parseJsonBytes32(j, ".server_random"); - proof.serverEphemeralKey = vm.parseJsonBytes(j, ".server_ephemeral_key"); - proof.transcriptRoot = vm.parseJsonBytes32(j, ".transcript_root"); - provenAt = vm.parseJsonUint(j, ".timestamp"); - proof.timestamp = provenAt; - proof.domainPath = vm.parseJsonBytes32Array(j, ".domain_path"); - proof.endpointPath = vm.parseJsonBytes32Array(j, ".endpoint_path"); - - for (uint256 i = 0; i < 16; i++) { - string memory base = string.concat(".claims[", vm.toString(i), "]"); - try this.probeKid(j, base) returns (string memory kid) { - IdentityJwksRoots.JwkClaim memory c; - c.kid = bytes(kid); - c.nB64url = bytes(vm.parseJsonString(j, string.concat(base, ".n_b64url"))); - c.jwkBytes = vm.parseJsonBytes(j, string.concat(base, ".jwk_bytes")); - c.jwkPath = vm.parseJsonBytes32Array(j, string.concat(base, ".jwk_path")); - claims.push(c); - } catch { - break; - } - } - require(claims.length > 0, "fixture carries no claims"); - } - - function probeKid(string memory j, string memory base) external view returns (string memory) { - return vm.parseJsonString(j, string.concat(base, ".kid")); - } - - function _noClaims() internal pure returns (IdentityJwksRoots.JwkClaim[] memory) { - return new IdentityJwksRoots.JwkClaim[](0); - } - - /// A reading signed by `TEST_NOTARY_PK`, naming whatever host is asked for. - /// The digest is over the TLS session alone — no contract appears in it, - /// which is why one reading is valid wherever the notary is trusted. - function _signedProof(bytes32 domainHash) internal view returns (IdentityJwksRoots.NotarizedJwksProof memory p) { - p.domainHash = domainHash; - p.clientRandom = keccak256("client"); - p.serverRandom = keccak256("server"); - p.serverEphemeralKey = bytes("ephemeral-key"); - p.transcriptRoot = keccak256("root"); - p.timestamp = block.timestamp; - p.domainPath = new bytes32[](0); - p.endpointPath = new bytes32[](0); - _sign(p); - } - - /// Sign whatever the proof currently says. Callers that edit the transcript - /// re-sign, since the root is inside the digest. - function _sign(IdentityJwksRoots.NotarizedJwksProof memory p) internal pure { - bytes32 digest = keccak256( - abi.encode( - p.domainHash, - p.clientRandom, - p.serverRandom, - keccak256(p.serverEphemeralKey), - p.transcriptRoot, - p.timestamp - ) - ); - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(TEST_NOTARY_PK, ethHash); - p.notarySignature = abi.encodePacked(r, s, v); + vm.deal(address(this), 100 ether); + vm.deal(keeper, 1 ether); } - // ─── What it accepts ──────────────────────────────────────────── + function _deployRoots(INotaryService service) private returns (IdentityJwksRoots) { + IdentityJwksRoots impl = new IdentityJwksRoots(); + return IdentityJwksRoots( + address(new ERC1967Proxy(address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, service)))) + ); + } - function test_aNotarizedReadingInstallsTheKeys() public { - roots.rotate(proof, claims); + // ─── Building a reading ───────────────────────────────────────── - for (uint256 i = 0; i < claims.length; i++) { - bytes32 kidHash = keccak256(claims[i].kid); - bytes32 modulusHash = roots.modulusOfKid(kidHash); - assertTrue(modulusHash != bytes32(0), "kid installed"); - assertEq(roots.trustedHashExpiresAt(modulusHash), block.timestamp + roots.DEFAULT_MODULUS_TTL()); - } + function _signWith(uint256 key, bytes memory attested) private pure returns (bytes memory) { + bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(attested))); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, ethHash); + return abi.encodePacked(r, s, v); } - /// Anyone may submit one. The proof carries its own authority — the notary - /// signed it — so gating the caller would only add a keeper to trust. - function test_anybodyMaySubmitARotation() public { - vm.prank(keeper); - roots.rotate(proof, claims); - - assertTrue(roots.modulusOfKid(keccak256(claims[0].kid)) != bytes32(0)); + function _sign(bytes memory attested) private pure returns (bytes memory) { + return _signWith(NOTARY_KEY, attested); } - /// Re-applying the same reading writes the same values. It must not revert: - /// with an open caller set, a revert would let a front-runner land one - /// claim and brick the honest keeper's batch. - function test_replayingAReadingIsIdempotent() public { - roots.rotate(proof, claims); - bytes32 first = roots.modulusOfKid(keccak256(claims[0].kid)); + /// The JWKS layout: one revealed range covering the whole direction, + /// nothing committed. + function _whole(bytes memory transcript) private pure returns (AttestationBuilder.Direction memory) { + return AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: transcript})), + commitments: AttestationBuilder.none(), + length: uint32(transcript.length) + }); + } - roots.rotate(proof, claims); - assertEq(roots.modulusOfKid(keccak256(claims[0].kid)), first); + function _empty() private pure returns (AttestationBuilder.Direction memory) { + return AttestationBuilder.Direction({ + revealed: new AttestationBuilder.Range[](0), commitments: AttestationBuilder.none(), length: 0 + }); } - // ─── What it refuses ──────────────────────────────────────────── + function _attest(bytes memory sent, bytes memory received, uint64 createdAt) private pure returns (bytes memory) { + return AttestationBuilder.encode(AUTHORITY, createdAt, _whole(sent), _whole(received)); + } - /// The signature is the whole authority, so a reading the current notary - /// did not sign is worth nothing. Rotation happens on the shared Notary - /// contract, and this list follows it. - function test_aReadingFromAnUnknownNotaryIsRefused() public { - vm.prank(owner); - notaryContract.setNotary(makeAddr("somebody else")); + /// A reading of `body`, framed by `Content-Length`, observed now. + function _reading(bytes memory body) private view returns (bytes memory) { + return _readingAt(body, _now()); + } - vm.expectRevert(IdentityJwksRoots.UnknownNotary.selector); - roots.rotate(proof, claims); + function _readingAt(bytes memory body, uint64 createdAt) private pure returns (bytes memory) { + return _attest(REQUEST, _contentLength(body), createdAt); } - /// The host is inside the signed digest, so it cannot be edited after the - /// fact: changing it makes the signature recover to somebody else. That is - /// what stops a notarized reading of one host being presented as another's. - function test_theHostCannotBeChangedAfterSigning() public { - IdentityJwksRoots.NotarizedJwksProof memory p = proof; - p.domainHash = keccak256("accounts.example.com"); + function _rotate(bytes memory attested) private { + roots.rotate{value: FEE}(attested, _sign(attested)); + } - vm.expectRevert(IdentityJwksRoots.UnknownNotary.selector); - roots.rotate(p, claims); + /// Signed BEFORE the expectation: `vm.sign` is an external call, and one + /// inside the call under test would consume the `expectRevert`. + function _refuses(bytes memory attested, bytes memory err) private { + bytes memory sig = _sign(attested); + vm.expectRevert(err); + roots.rotate{value: FEE}(attested, sig); } - /// And a reading genuinely signed for another host is refused on its - /// merits. Signed here with a key this test holds, since the fixture's - /// notary key is not in the repository. - /// - /// No claims are needed: the host is checked before any key is read, which - /// is the order that matters — the expensive parsing never runs for a - /// reading that was never about Google. - function test_aReadingGenuinelySignedForAnotherHostIsRefused() public { - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); + function _refuses(bytes memory attested, bytes4 selector) private { + _refuses(attested, abi.encodeWithSelector(selector)); + } - IdentityJwksRoots.NotarizedJwksProof memory p = _signedProof(keccak256("accounts.example.com")); + // ─── Framing the response ─────────────────────────────────────── - vm.expectRevert(IdentityJwksRoots.WrongDomain.selector); - roots.rotate(p, _noClaims()); + function _contentLength(bytes memory body) private pure returns (bytes memory) { + return + abi.encodePacked( + STATUS_OK, CONTENT_TYPE, "content-length: ", vm.toString(body.length), "\r\n", "\r\n", body + ); } - /// The right host, but a transcript that does not carry it as a leaf. The - /// signature covers the root, not what is under it. - function test_aReadingWithoutTheHostLeafIsRefused() public { - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); + /// Chunked transfer coding, `size` bytes per chunk, as Google serves it. + function _chunked(bytes memory body, uint256 size) private pure returns (bytes memory) { + return abi.encodePacked(STATUS_OK, CONTENT_TYPE, "transfer-encoding: chunked\r\n", "\r\n", _chunks(body, size)); + } - IdentityJwksRoots.NotarizedJwksProof memory p = _signedProof(keccak256(bytes("www.googleapis.com"))); + function _chunks(bytes memory body, uint256 size) private pure returns (bytes memory out) { + for (uint256 at = 0; at < body.length; at += size) { + uint256 take = body.length - at < size ? body.length - at : size; + bytes memory chunk = new bytes(take); + for (uint256 i = 0; i < take; ++i) { + chunk[i] = body[at + i]; + } + out = abi.encodePacked(out, _hex(take), "\r\n", chunk, "\r\n"); + } + out = abi.encodePacked(out, "0\r\n\r\n"); + } - vm.expectRevert(IdentityJwksRoots.MerkleMismatch.selector); - roots.rotate(p, _noClaims()); + function _hex(uint256 v) private pure returns (bytes memory out) { + if (v == 0) return "0"; + bytes memory digits = "0123456789abcdef"; + while (v != 0) { + out = abi.encodePacked(digits[v & 0xf], out); + v >>= 4; + } } - /// Past the window the reading says nothing about what Google publishes - /// now, which is the only question this list answers. - function test_aStaleReadingIsRefused() public { - vm.warp(provenAt + roots.FRESHNESS_WINDOW() + 1); + // ─── Building a key set ───────────────────────────────────────── - vm.expectRevert(IdentityJwksRoots.StaleProof.selector); - roots.rotate(proof, claims); + /// One JWK the way Google writes them, compact. + function _jwk(string memory kid, string memory n) private pure returns (bytes memory) { + return abi.encodePacked('{"kty":"RSA","use":"sig","kid":"', kid, '","n":"', n, '","e":"AQAB","alg":"RS256"}'); } - /// Rotation is open, so an older reading replayed inside the window would - /// otherwise roll a kid back to a modulus Google has retired — and re-stamp - /// it for another full TTL. - function test_anOlderReadingCannotRollAKidBack() public { - roots.rotate(proof, claims); + function _body(bytes memory jwks) private pure returns (bytes memory) { + return abi.encodePacked('{"keys":[', jwks, "]}"); + } - bytes32 kidHash = keccak256(claims[0].kid); - bytes32 current = roots.modulusOfKid(kidHash); - uint256 currentExpiry = roots.expiresAtKid(kidHash); + function _oneKey(string memory kid, string memory seed) private pure returns (bytes memory) { + return _body(_jwk(kid, _modulus(seed))); + } - // The same claims, presented as if proved a minute earlier. The - // timestamp is inside the signed digest, so editing it makes the - // signature recover to somebody else — which is the guard working, one - // step earlier than the monotonic check behind it. - IdentityJwksRoots.NotarizedJwksProof memory older = proof; - older.timestamp = provenAt - 60; + /// A 2048-bit modulus derived from `seed`, base64url without padding: 256 + /// bytes encode to exactly 342 characters. Different seeds give different + /// keys, which is the only property the caller needs. + function _modulus(string memory seed) private pure returns (string memory) { + return string(_b64url(_bytes(seed, 256))); + } - vm.expectRevert(IdentityJwksRoots.UnknownNotary.selector); - roots.rotate(older, claims); + function _bytes(string memory seed, uint256 count) private pure returns (bytes memory out) { + out = new bytes(count); + for (uint256 i = 0; i < count; ++i) { + if (i % 32 == 0) { + bytes32 word = keccak256(abi.encodePacked(seed, i / 32)); + for (uint256 j = 0; j < 32 && i + j < count; ++j) { + out[i + j] = word[j]; + } + } + } + } - assertEq(roots.modulusOfKid(kidHash), current, "kid unchanged"); - assertEq(roots.expiresAtKid(kidHash), currentExpiry, "expiry not re-stamped"); + function _b64url(bytes memory data) private pure returns (bytes memory out) { + bytes memory alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + uint256 full = data.length / 3; + uint256 tail = data.length % 3; + out = new bytes(full * 4 + (tail == 0 ? 0 : tail + 1)); + uint256 o; + for (uint256 i = 0; i < full; ++i) { + uint256 v = (uint256(uint8(data[3 * i])) << 16) | (uint256(uint8(data[3 * i + 1])) << 8) + | uint256(uint8(data[3 * i + 2])); + out[o++] = alphabet[(v >> 18) & 63]; + out[o++] = alphabet[(v >> 12) & 63]; + out[o++] = alphabet[(v >> 6) & 63]; + out[o++] = alphabet[v & 63]; + } + if (tail == 1) { + uint256 v = uint256(uint8(data[3 * full])) << 16; + out[o++] = alphabet[(v >> 18) & 63]; + out[o++] = alphabet[(v >> 12) & 63]; + } else if (tail == 2) { + uint256 v = (uint256(uint8(data[3 * full])) << 16) | (uint256(uint8(data[3 * full + 1])) << 8); + out[o++] = alphabet[(v >> 18) & 63]; + out[o++] = alphabet[(v >> 12) & 63]; + out[o++] = alphabet[(v >> 6) & 63]; + } } - // ─── Losing trust ─────────────────────────────────────────────── + /// `count` keys, `kid-0` .. `kid-`, each with its own modulus. + function _manyKeys(uint256 count) private pure returns (bytes memory list) { + for (uint256 i = 0; i < count; ++i) { + string memory kid = string.concat("kid-", vm.toString(i)); + list = abi.encodePacked(list, i == 0 ? "" : ",", _jwk(kid, _modulus(kid))); + } + return _body(list); + } - /// The verifier resolves by modulus, not by kid, so a key that a rotation - /// replaced has to stop being trusted at that moment. Leaving it keeps a - /// retired key usable for the rest of its thirty-day stamp — which is the - /// exact window a compromised key would be used in. - function test_rotatingAKidRetiresTheKeyItCarried() public { - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); + /// Install `kid` carrying a key derived from `seed`, observed now, and + /// return the modulus hash it wrote. + function _installKid(string memory kid, string memory seed) private returns (bytes32) { + _rotate(_reading(_oneKey(kid, seed))); + return roots.modulusOfKid(keccak256(bytes(kid))); + } - // Install a first key for this kid, then a second one for the same kid. - bytes32 first = _installKid("kid-1", "modulus-one"); - assertGt(roots.trustedHashExpiresAt(first), 0, "first key trusted"); + // ─── What it accepts ──────────────────────────────────────────── - bytes32 second = _installKid("kid-1", "modulus-two"); - assertGt(roots.trustedHashExpiresAt(second), 0, "second key trusted"); - assertEq(roots.trustedHashExpiresAt(first), 0, "the replaced key is no longer trusted"); + function test_aContentLengthFramedReadingInstallsTheKeys() public { + bytes memory body = _body(abi.encodePacked(_jwk("kid-1", _modulus("one")), ",", _jwk("kid-2", _modulus("two")))); + _rotate(_reading(body)); + + bytes32 first = roots.modulusOfKid(keccak256("kid-1")); + bytes32 second = roots.modulusOfKid(keccak256("kid-2")); + assertTrue(first != bytes32(0), "kid-1 installed"); + assertTrue(second != bytes32(0), "kid-2 installed"); + assertTrue(first != second, "distinct keys"); + assertEq(roots.trustedHashExpiresAt(first), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + assertEq(roots.trustedHashExpiresAt(second), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + assertEq(roots.expiresAtKid(keccak256("kid-1")), block.timestamp + roots.DEFAULT_MODULUS_TTL()); } - /// The case that cannot wait for a rotation: a key Google has not retired, - /// or has retired in a way this list has not seen yet. - function test_theOwnerCanUntrustAKeyOutright() public { - roots.rotate(proof, claims); + /// Google serves the key set chunked. The chunk size here cuts the first + /// modulus value three times, so the de-chunker is what reassembles it; + /// the same body under `Content-Length` yields the same hash, which is the + /// framing being transparent. + function test_aChunkedReadingInstallsTheKeys() public { + bytes memory body = _oneKey("kid-1", "one"); + _rotate(_attest(REQUEST, _chunked(body, 100), _now())); + bytes32 viaChunked = roots.modulusOfKid(keccak256("kid-1")); + assertTrue(viaChunked != bytes32(0), "installed through chunked framing"); + + vm.warp(block.timestamp + 1); + _rotate(_reading(body)); + assertEq(roots.modulusOfKid(keccak256("kid-1")), viaChunked, "framing changed the key"); + } - bytes32 kidHash = keccak256(claims[0].kid); - bytes32 modulusHash = roots.modulusOfKid(kidHash); - assertGt(roots.trustedHashExpiresAt(modulusHash), 0); + /// The document this contract exists to read: Google's body, verbatim, + /// pretty-printed with a space after every colon, in the framing Google + /// uses. + function test_googlesRealBodyIsAccepted() public { + bytes memory body = bytes(vm.readFile(GOOGLE_JWKS)); + assertEq(string(_slice(body, 0, 13)), "{\n \"keys\": [", "the fixture is Google's pretty-printed body"); - vm.prank(owner); - roots.untrustModulus(modulusHash); + _rotate(_attest(REQUEST, _chunked(body, 512), _now())); - assertEq(roots.trustedHashExpiresAt(modulusHash), 0, "no longer trusted"); + assertTrue(roots.modulusOfKid(GOOGLE_KID_1) != bytes32(0), "first Google key installed"); + assertTrue(roots.modulusOfKid(GOOGLE_KID_2) != bytes32(0), "second Google key installed"); + assertEq(roots.currentRoots().length, 2); + assertFalse(roots.needsRotation()); } - function test_onlyTheOwnerUntrusts() public { - vm.prank(keeper); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, keeper)); - roots.untrustModulus(bytes32(uint256(1))); + /// Cross-language pin: the keeper's `decision::modulus_hash` and the + /// Google circuit produce this value for this modulus. + function test_theLimbHashMatchesTheKeeper() public { + _rotate(_reading(_body(_jwk("keeper", KEEPER_MODULUS)))); + assertEq(roots.modulusOfKid(keccak256("keeper")), KEEPER_MODULUS_HASH); + assertGt(roots.trustedHashExpiresAt(KEEPER_MODULUS_HASH), 0); } - /// Install `kid` carrying a key derived from `seed`, and return the modulus - /// hash it wrote. - function _installKid(string memory kid, string memory seed) internal returns (bytes32) { - (IdentityJwksRoots.NotarizedJwksProof memory p, IdentityJwksRoots.JwkClaim[] memory one) = - _kidReading(kid, seed, block.timestamp); - roots.rotate(p, one); - return roots.modulusOfKid(keccak256(bytes(kid))); + /// Anyone may submit one. The reading carries its own authority -- the + /// notary signed it -- so gating the caller would only add a keeper to + /// trust. + function test_anybodyMaySubmitARotation() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory sig = _sign(attested); + vm.prank(keeper); + roots.rotate{value: FEE}(attested, sig); + assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); } - /// Build a full notarized reading that carries one key, signed by the - /// notary key this test holds, timestamped `provedAt`. - /// - /// The fixture holds one reading of Google's real keys, and a rotation only - /// retires something when a kid comes back carrying a DIFFERENT key — which - /// no single reading contains. So this builds a second reading: a JWKS leaf - /// of its own, a transcript that carries it beside the domain and endpoint - /// leaves, and the notary key this test holds. - function _kidReading(string memory kid, string memory seed, uint256 provedAt) - internal - view - returns (IdentityJwksRoots.NotarizedJwksProof memory p, IdentityJwksRoots.JwkClaim[] memory one) - { - bytes memory nB64 = _syntheticModulus(seed); - bytes memory jwk = abi.encodePacked('{"kid":"', kid, '","n":"', nB64, '"}'); - - // Three leaves under one root: domain and endpoint, which every reading - // must carry, and this key. Siblings are hashed in sorted order, so a - // path is just the list of them from the leaf upward. - bytes32 domainLeaf = _leaf("domain:", bytes("www.googleapis.com")); - bytes32 endpointLeaf = _leaf("endpoint:", bytes("/oauth2/v3/certs")); - bytes32 jwkLeaf = _leaf("recv:", jwk); - bytes32 pair = _hashPair(domainLeaf, endpointLeaf); - bytes32 root = _hashPair(pair, jwkLeaf); - - p = _signedProof(EXPECTED_DOMAIN_HASH); - p.transcriptRoot = root; - p.domainPath = _path(endpointLeaf, jwkLeaf); - p.endpointPath = _path(domainLeaf, jwkLeaf); - p.timestamp = provedAt; - _sign(p); - - one = new IdentityJwksRoots.JwkClaim[](1); - one[0].jwkBytes = jwk; - one[0].jwkPath = _path(pair); - one[0].kid = bytes(kid); - one[0].nB64url = nB64; - } - - /// A 2048-bit modulus, written straight in base64url so the test needs no - /// encoder: 342 characters decode to exactly the 256 bytes the contract - /// requires. Different seeds give different keys, which is the only property - /// the caller needs. - function _syntheticModulus(string memory seed) internal pure returns (bytes memory out) { - bytes memory alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - bytes32 filler = keccak256(bytes(seed)); - out = new bytes(342); - for (uint256 i = 0; i < 342; i++) { - out[i] = alphabet[uint8(filler[i % 32]) & 0x3f]; - } + /// The Notary Fee is the one thing a rotation costs beyond gas, and it + /// accrues where every other attestation's does. + function test_theRotationPaysTheNotaryFee() public { + assertEq(roots.quoteRotation(), FEE); + _installKid("kid-1", "one"); + assertEq(address(notary).balance, FEE, "the fee reached the Notary Service"); + assertEq(address(roots).balance, 0, "nothing stays with the list"); } - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } + /// Re-applying the same reading writes the same values. It must not + /// revert: with an open caller set, a revert would let a front-runner land + /// the reading first and brick the honest keeper. + function test_replayingAReadingIsIdempotent() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + _rotate(attested); + bytes32 first = roots.modulusOfKid(keccak256("kid-1")); - function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); + _rotate(attested); + assertEq(roots.modulusOfKid(keccak256("kid-1")), first); } - function _path(bytes32 a) internal pure returns (bytes32[] memory p) { - p = new bytes32[](1); - p[0] = a; - } + /// Resubmitting the exact reading already applied writes nothing -- in + /// particular it does NOT re-stamp the TTL, so spamming one reading for + /// its whole freshness window cannot stretch trust by even a second. + function test_anIdenticalResubmissionDoesNotRestampTheTtl() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + _rotate(attested); + uint256 expiry = roots.expiresAtKid(keccak256("kid-1")); - function _path(bytes32 a, bytes32 b) internal pure returns (bytes32[] memory p) { - p = new bytes32[](2); - p[0] = a; - p[1] = b; - } + vm.warp(block.timestamp + 30 minutes); // still inside the window + vm.recordLogs(); + _rotate(attested); - // ─── Monotonicity under replay ────────────────────────────────── + assertEq(roots.expiresAtKid(keccak256("kid-1")), expiry, "replay re-stamped the TTL"); + assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + } /// The rollback that matters: a GENUINELY signed older reading, replayed /// inside its freshness window after a newer one has landed. The signature - /// verifies; the per-kid monotonic stamp is what refuses the regression — + /// verifies; the per-kid monotonic stamp is what refuses the regression -- /// silently, so a keeper batch is never bricked. function test_aGenuinelySignedOlderReadingCannotRegressState() public { - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); - - // The older reading exists first (proved 30 min ago), but the NEWER - // one lands on chain first. - (IdentityJwksRoots.NotarizedJwksProof memory oldP, IdentityJwksRoots.JwkClaim[] memory oldC) = - _kidReading("kid-1", "retired-modulus", block.timestamp - 30 minutes); + bytes memory older = _readingAt(_oneKey("kid-1", "retired-modulus"), _now() - 30 minutes); bytes32 newModulus = _installKid("kid-1", "current-modulus"); - bytes32 kidHash = keccak256(bytes("kid-1")); + bytes32 kidHash = keccak256("kid-1"); uint256 stamp = roots.rotatedAtKid(kidHash); uint256 expiry = roots.expiresAtKid(kidHash); - vm.prank(keeper); - roots.rotate(oldP, oldC); // must not revert, must not apply + _rotate(older); // must not revert, must not apply assertEq(roots.modulusOfKid(kidHash), newModulus, "older reading rolled the kid back"); assertEq(roots.rotatedAtKid(kidHash), stamp, "older reading overwrote the stamp"); @@ -385,50 +364,69 @@ contract IdentityJwksRootsTest is Test { assertEq(roots.trustedHashExpiresAt(newModulus), expiry, "current key lost trust"); } - /// Resubmitting the exact reading already applied writes nothing — in - /// particular it does NOT re-stamp the TTL, so spamming one proof for its - /// whole freshness window cannot stretch trust by even a second. - function test_anIdenticalResubmissionDoesNotRestampTheTtl() public { - roots.rotate(proof, claims); - bytes32 kidHash = keccak256(claims[0].kid); - uint256 expiry = roots.expiresAtKid(kidHash); + /// The freshest-reading mark only moves forward, whoever submits. + function test_freshestObservedAtIsMonotonic() public { + assertEq(roots.freshestObservedAt(), 0); + uint64 first = _now(); + _rotate(_readingAt(_oneKey("kid-1", "one"), first)); + assertEq(roots.freshestObservedAt(), first); - vm.warp(block.timestamp + 30 minutes); // still inside the window - vm.prank(keeper); - roots.rotate(proof, claims); + _rotate(_readingAt(_oneKey("kid-1", "one"), first - 10 minutes)); + assertEq(roots.freshestObservedAt(), first, "an older reading moved the mark"); - assertEq(roots.expiresAtKid(kidHash), expiry, "replay re-stamped the TTL"); + vm.warp(first + 20 minutes); + _rotate(_readingAt(_oneKey("kid-1", "one"), first + 10 minutes)); + assertEq(roots.freshestObservedAt(), first + 10 minutes, "a newer reading must move the mark"); } - /// The freshest-reading mark only moves forward, whoever submits. - function test_freshestObservedAtIsMonotonic() public { - assertEq(roots.freshestObservedAt(), 0); - roots.rotate(proof, claims); - assertEq(roots.freshestObservedAt(), provenAt); + // ─── Losing trust ─────────────────────────────────────────────── + + /// The verifier resolves by modulus, not by kid, so a key that a rotation + /// replaced has to stop being trusted at that moment. Leaving it keeps a + /// retired key usable for the rest of its thirty-day stamp -- which is the + /// exact window a compromised key would be used in. + function test_rotatingAKidRetiresTheKeyItCarried() public { + bytes32 first = _installKid("kid-1", "modulus-one"); + assertGt(roots.trustedHashExpiresAt(first), 0, "first key trusted"); - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); - (IdentityJwksRoots.NotarizedJwksProof memory oldP, IdentityJwksRoots.JwkClaim[] memory oldC) = - _kidReading("kid-1", "whatever", provenAt - 10 minutes); - roots.rotate(oldP, oldC); - assertEq(roots.freshestObservedAt(), provenAt, "an older reading moved the mark"); + vm.warp(block.timestamp + 1 minutes); + vm.expectEmit(true, false, false, true); + emit IdentityJwksRoots.ModulusUntrusted(first); + bytes32 second = _installKid("kid-1", "modulus-two"); - vm.warp(provenAt + 20 minutes); - (IdentityJwksRoots.NotarizedJwksProof memory newP, IdentityJwksRoots.JwkClaim[] memory newC) = - _kidReading("kid-1", "whatever", provenAt + 10 minutes); - roots.rotate(newP, newC); - assertEq(roots.freshestObservedAt(), provenAt + 10 minutes, "a newer reading must move the mark"); + assertGt(roots.trustedHashExpiresAt(second), 0, "second key trusted"); + assertEq(roots.trustedHashExpiresAt(first), 0, "the replaced key is no longer trusted"); + } + + /// The case that cannot wait for a rotation: a key Google has not retired, + /// or has retired in a way this list has not seen yet. + function test_theOwnerCanUntrustAKeyOutright() public { + bytes32 modulusHash = _installKid("kid-1", "one"); + assertGt(roots.trustedHashExpiresAt(modulusHash), 0); + + vm.prank(OWNER); + vm.expectEmit(true, false, false, true); + emit IdentityJwksRoots.ModulusUntrusted(modulusHash); + roots.untrustModulus(modulusHash); + + assertEq(roots.trustedHashExpiresAt(modulusHash), 0, "no longer trusted"); + } + + function test_onlyTheOwnerUntrusts() public { + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, keeper)); + roots.untrustModulus(bytes32(uint256(1))); } // ─── Expiry and pruning ───────────────────────────────────────── /// Time alone retires a key: past its stamp the verifier-facing read says - /// expired, with no transaction from anyone. `prune()` — callable by - /// anyone — then reclaims the bookkeeping. + /// expired, with no transaction from anyone. `prune()` -- callable by + /// anyone -- then reclaims the bookkeeping. function test_anExpiredKeyIsPrunableByAnyone() public { - roots.rotate(proof, claims); - bytes32 kidHash = keccak256(claims[0].kid); - bytes32 modulusHash = roots.modulusOfKid(kidHash); + uint64 provenAt = _now(); + bytes32 modulusHash = _installKid("kid-1", "one"); + bytes32 kidHash = keccak256("kid-1"); uint256 expiry = roots.expiresAtKid(kidHash); vm.warp(expiry); // the stamp is spent; use-site checks already refuse it @@ -449,43 +447,33 @@ contract IdentityJwksRootsTest is Test { /// Pruning is a no-op while the keys are alive. function test_pruneLeavesLiveKeysAlone() public { - roots.rotate(proof, claims); - uint256 before = roots.currentRoots().length; + bytes32 modulusHash = _installKid("kid-1", "one"); vm.prank(keeper); roots.prune(); - assertEq(roots.currentRoots().length, before, "a live key was pruned"); - assertGt(roots.trustedHashExpiresAt(roots.modulusOfKid(keccak256(claims[0].kid))), 0); + assertEq(roots.currentRoots().length, 1, "a live key was pruned"); + assertGt(roots.trustedHashExpiresAt(modulusHash), 0); } /// The tracked set is capped, and the cap defends itself: a full set first /// sheds its expired entries, and only a set full of LIVE keys refuses. /// Since every kid must arrive inside a notarized reading of Google's own - /// JWKS, an adversary cannot even choose the kids — this is the ceiling on - /// Google's, not the submitter's, behavior. + /// JWKS, an adversary cannot even choose the kids -- this is the ceiling + /// on Google's, not the submitter's, behavior. function test_theTrackedSetIsBoundedAndSelfPrunes() public { - vm.prank(owner); - notaryContract.setNotary(vm.addr(TEST_NOTARY_PK)); - uint256 max = roots.MAX_TRACKED_KIDS(); - for (uint256 i = 0; i < max; i++) { - _installKid(string.concat("kid-", vm.toString(i)), vm.toString(i)); - } + _rotate(_reading(_manyKeys(max))); assertEq(roots.currentRoots().length, max, "set should sit at the cap"); // A brand-new kid while every slot holds a live key: refused. - (IdentityJwksRoots.NotarizedJwksProof memory p, IdentityJwksRoots.JwkClaim[] memory one) = - _kidReading("kid-overflow", "overflow", block.timestamp); - vm.expectRevert(IdentityJwksRoots.TooManyKids.selector); - roots.rotate(p, one); + _refuses(_reading(_oneKey("kid-overflow", "overflow")), IdentityJwksRoots.TooManyKids.selector); // Once the old stamps are spent, the same insert prunes its own room. vm.warp(block.timestamp + roots.DEFAULT_MODULUS_TTL() + 1); - (p, one) = _kidReading("kid-overflow", "overflow", block.timestamp); - roots.rotate(p, one); + _rotate(_reading(_oneKey("kid-overflow", "overflow"))); assertEq(roots.currentRoots().length, 1, "expired entries should have been shed"); - assertEq(roots.currentRoots()[0].kidHash, keccak256(bytes("kid-overflow"))); + assertEq(roots.currentRoots()[0].kidHash, keccak256("kid-overflow")); } // ─── What a keeper reads ──────────────────────────────────────── @@ -493,11 +481,13 @@ contract IdentityJwksRootsTest is Test { /// One call, the whole state: every tracked key with its provenance and /// expiry. function test_currentRootsDescribesTheWholeList() public { - roots.rotate(proof, claims); + uint64 provenAt = _now() - 5 minutes; + _rotate(_readingAt(_manyKeys(3), provenAt)); IdentityJwksRoots.RootInfo[] memory infos = roots.currentRoots(); - assertEq(infos.length, claims.length, "every claim should be listed"); - for (uint256 i = 0; i < infos.length; i++) { + assertEq(infos.length, 3, "every key should be listed"); + for (uint256 i = 0; i < infos.length; ++i) { + assertEq(infos[i].kidHash, keccak256(bytes(string.concat("kid-", vm.toString(i))))); assertEq(infos[i].modulusHash, roots.modulusOfKid(infos[i].kidHash)); assertEq(infos[i].observedAt, provenAt, "provenance is the reading's timestamp"); assertEq(infos[i].expiresAt, roots.trustedHashExpiresAt(infos[i].modulusHash)); @@ -506,47 +496,552 @@ contract IdentityJwksRootsTest is Test { /// The single bit a keeper polls: true until the first rotation, false /// while a key has RENEWAL_MARGIN of trusted runway, true again as the - /// runway shortens — and long before anything actually expires. + /// runway shortens -- and long before anything actually expires. function test_needsRotationTracksTheTrustedRunway() public { assertTrue(roots.needsRotation(), "an empty list needs a rotation"); - roots.rotate(proof, claims); + _installKid("kid-1", "one"); assertFalse(roots.needsRotation(), "a fresh rotation buys quiet"); - bytes32 kidHash = keccak256(claims[0].kid); - uint256 expiry = roots.expiresAtKid(kidHash); + uint256 expiry = roots.expiresAtKid(keccak256("kid-1")); vm.warp(expiry - roots.RENEWAL_MARGIN()); assertTrue(roots.needsRotation(), "the margin should trip before expiry does"); } - /// Each applied key announces itself with the reading's own timestamp, so - /// a bot can follow the list from logs alone. + /// Each applied key announces itself with the reading's own timestamp -- + /// the notary's clock, not the block's -- so a bot can follow the list + /// from logs alone. function test_rotationEmitsRootAppliedWithProvenance() public { - bytes32 kidHash = keccak256(claims[0].kid); - vm.expectEmit(true, false, false, false); - emit IdentityJwksRoots.RootApplied(kidHash, bytes32(0), provenAt, 0); - roots.rotate(proof, claims); + uint64 provenAt = _now() - 10 minutes; + bytes32 kidHash = keccak256("kid-1"); + + vm.recordLogs(); + _rotate(_readingAt(_oneKey("kid-1", "one"), provenAt)); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 topic = keccak256("RootApplied(bytes32,bytes32,uint256,uint256)"); + uint256 seen; + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].emitter != address(roots) || logs[i].topics[0] != topic) continue; + ++seen; + assertEq(logs[i].topics[1], kidHash); + assertEq(logs[i].topics[2], roots.modulusOfKid(kidHash)); + (uint256 observedAt, uint256 expiresAt) = abi.decode(logs[i].data, (uint256, uint256)); + assertEq(observedAt, provenAt, "observedAt is the notary's clock"); + assertEq(expiresAt, block.timestamp + roots.DEFAULT_MODULUS_TTL()); + } + assertEq(seen, 1, "one RootApplied per key"); } // ─── Administration ───────────────────────────────────────────── - function test_onlyTheOwnerRotatesTheNotary() public { + /// The Notary Service is the trust root. Pointing at another one moves + /// the whole list under that service's keys and fee. + function test_theOwnerCanPointAtAnotherNotaryService() public { + uint256 otherKey = 0xC0FFEE; + NotaryService otherImpl = new NotaryService(); + NotaryService other = NotaryService( + address( + new ERC1967Proxy( + address(otherImpl), abi.encodeCall(NotaryService.initialize, (OWNER, vm.addr(otherKey), 2 * FEE)) + ) + ) + ); + + vm.prank(OWNER); + vm.expectEmit(false, false, false, true); + emit IdentityJwksRoots.NotaryServiceChanged(address(other)); + roots.setNotaryService(INotaryService(address(other))); + + assertEq(roots.notaryService(), address(other)); + assertEq(roots.quoteRotation(), 2 * FEE); + + // The old service's key no longer vouches for anything here... + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory oldSig = _sign(attested); + vm.expectRevert(abi.encodeWithSelector(NotaryService.UntrustedNotary.selector, vm.addr(NOTARY_KEY))); + roots.rotate{value: 2 * FEE}(attested, oldSig); + + // ...and the new one's does, at the new fee. + roots.rotate{value: 2 * FEE}(attested, _signWith(otherKey, attested)); + assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); + assertEq(address(other).balance, 2 * FEE); + } + + function test_onlyTheOwnerSetsTheNotaryService() public { vm.prank(keeper); vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, keeper)); - notaryContract.setNotary(keeper); + roots.setNotaryService(INotaryService(address(0xD00D))); } - /// The list reads the notary through the shared Notary contract. - function test_theNotaryIsReadThroughTheNotaryContract() public view { - assertEq(address(roots.notaryContract()), address(notaryContract)); - assertEq(roots.notary(), NOTARY); + function test_aZeroNotaryServiceIsRefused() public { + vm.prank(OWNER); + vm.expectRevert(IdentityJwksRoots.ZeroAddress.selector); + roots.setNotaryService(INotaryService(address(0))); + + IdentityJwksRoots impl = new IdentityJwksRoots(); + vm.expectRevert(IdentityJwksRoots.ZeroAddress.selector); + new ERC1967Proxy( + address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, INotaryService(address(0)))) + ); } /// Renouncing would freeze the list while Google's keys keep expiring, so /// the naming system would stop accepting Google proofs with no way back. function test_renouncingIsDisabled() public { - vm.prank(owner); + vm.prank(OWNER); vm.expectRevert(bytes("renounce disabled")); roots.renounceOwnership(); } + + // ─── What it refuses: the fee and the notary ──────────────────── + + /// An exact match, both ways. Underpaying is obvious; overpaying must fail + /// too, so there is no overpayment to refund and no silent overcharge. + function test_refusesAnyValueOtherThanTheFee() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory sig = _sign(attested); + + vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, FEE - 1)); + roots.rotate{value: FEE - 1}(attested, sig); + + vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, FEE + 1)); + roots.rotate{value: FEE + 1}(attested, sig); + + vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, 0)); + roots.rotate(attested, sig); + } + + /// The signature is the whole authority, so a reading the trusted notary + /// did not sign is worth nothing. Trust lives on the Notary Service, and + /// this list follows it. + function test_refusesAnUntrustedNotaryKey() public { + uint256 stranger = 0xB0B; + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory sig = _signWith(stranger, attested); + vm.expectRevert(abi.encodeWithSelector(NotaryService.UntrustedNotary.selector, vm.addr(stranger))); + roots.rotate{value: FEE}(attested, sig); + } + + /// The signature is genuine and the key is trusted, but one byte of the + /// record differs -- so the digest the service derives differs and + /// recovery lands on nobody. Nothing in the record can be edited after + /// signing: not the host, not the clock, not a modulus. + function test_refusesATamperedAttestedByte() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory sig = _sign(attested); + attested[attested.length - 40] = bytes1(uint8(attested[attested.length - 40]) ^ 0x01); + vm.expectPartialRevert(NotaryService.UntrustedNotary.selector); + roots.rotate{value: FEE}(attested, sig); + } + + function test_refusesAMalformedSignature() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + vm.expectRevert(NotaryService.MalformedSignature.selector); + roots.rotate{value: FEE}(attested, hex"1234"); + } + + /// A rejected rotation leaves no fee behind. + function test_aRejectedRotationDeliversNoFee() public { + bytes memory attested = _reading(_oneKey("kid-1", "one")); + vm.expectRevert(NotaryService.MalformedSignature.selector); + roots.rotate{value: FEE}(attested, hex"1234"); + assertEq(address(notary).balance, 0, "a rejection kept the fee"); + assertEq(address(roots).balance, 0); + } + + // ─── What it refuses: the session ─────────────────────────────── + + /// A reading genuinely signed for another host is refused on its merits. + /// The authority is the one field the notary observed rather than was + /// told, and it is checked before any byte of the transcript is read. + function test_refusesAnotherAuthority() public { + bytes32 other = keccak256("storage.googleapis.com"); + bytes memory attested = + AttestationBuilder.encode(other, _now(), _whole(REQUEST), _whole(_contentLength(_oneKey("kid-1", "one")))); + _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.WrongAuthority.selector, AUTHORITY, other)); + } + + function test_refusesAReadingFromTheFuture() public { + uint64 ahead = _now() + uint64(roots.CLOCK_SKEW_GRACE()) + 1; + _refuses(_readingAt(_oneKey("kid-1", "one"), ahead), IdentityJwksRoots.FutureProof.selector); + // And accepts one exactly at the grace, which is the boundary. + _rotate(_readingAt(_oneKey("kid-1", "one"), ahead - 1)); + } + + /// Past the window the reading says nothing about what Google publishes + /// now, which is the only question this list answers. + function test_refusesAStaleReading() public { + uint64 provenAt = _now(); + bytes memory attested = _readingAt(_oneKey("kid-1", "one"), provenAt); + vm.warp(provenAt + roots.FRESHNESS_WINDOW() + 1); + _refuses(attested, IdentityJwksRoots.StaleProof.selector); + } + + /// Both directions must tile the signed length. A gap is where a prover + /// hides bytes, and this contract reads the join as the document. + function test_refusesACoverageGapInTheRequest() public { + uint32 len = uint32(REQUEST.length); + AttestationBuilder.Direction memory sent = AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: _slice(REQUEST, 0, len - 1)})), + commitments: AttestationBuilder.none(), + length: len + }); + bytes memory attested = + AttestationBuilder.encode(AUTHORITY, _now(), sent, _whole(_contentLength(_oneKey("kid-1", "one")))); + _refuses(attested, abi.encodeWithSelector(CeremonyAttestation.CoverageGap.selector, len - 1, len)); + } + + function test_refusesACoverageGapInTheResponse() public { + bytes memory response = _contentLength(_oneKey("kid-1", "one")); + uint32 len = uint32(response.length); + AttestationBuilder.Direction memory received = AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 1, value: _slice(response, 1, len)})), + commitments: AttestationBuilder.none(), + length: len + }); + bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _whole(REQUEST), received); + _refuses(attested, abi.encodeWithSelector(CeremonyAttestation.CoverageGap.selector, 0, 1)); + } + + /// A commitment tiles like a revealed range, so coverage passes -- and the + /// bytes behind it are exactly what this contract must not read around. + function test_refusesAHiddenByteInTheRequest() public { + uint32 len = uint32(REQUEST.length); + AttestationBuilder.Direction memory sent = AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: REQUEST})), + commitments: AttestationBuilder.one( + AttestationBuilder.Commitment({start: len, end: len + 4, value: bytes32(uint256(0x7777))}) + ), + length: len + 4 + }); + bytes memory attested = + AttestationBuilder.encode(AUTHORITY, _now(), sent, _whole(_contentLength(_oneKey("kid-1", "one")))); + _refuses(attested, IdentityJwksRoots.HiddenBytes.selector); + } + + function test_refusesAHiddenByteInTheResponse() public { + bytes memory response = _contentLength(_oneKey("kid-1", "one")); + uint32 len = uint32(response.length); + AttestationBuilder.Direction memory received = AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: response})), + commitments: AttestationBuilder.one( + AttestationBuilder.Commitment({start: len, end: len + 4, value: bytes32(uint256(0x7777))}) + ), + length: len + 4 + }); + bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _whole(REQUEST), received); + _refuses(attested, IdentityJwksRoots.HiddenBytes.selector); + } + + /// An empty direction passes coverage against a zero signed length; it is + /// named rather than left to an out-of-bounds panic. + function test_refusesAnEmptyRequest() public { + bytes memory attested = + AttestationBuilder.encode(AUTHORITY, _now(), _empty(), _whole(_contentLength(_oneKey("kid-1", "one")))); + _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); + } + + function test_refusesAnEmptyResponse() public { + bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _whole(REQUEST), _empty()); + _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); + } + + // ─── What it refuses: the request ─────────────────────────────── + + function _withRequest(bytes memory request) private view returns (bytes memory) { + return _attest(request, _contentLength(_oneKey("kid-1", "one")), _now()); + } + + function test_refusesAPostRequestLine() public { + bytes memory request = + abi.encodePacked("POST /oauth2/v3/certs HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length)); + _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + } + + /// The path separates the key set from everything else the host serves. + function test_refusesAnotherPath() public { + bytes memory request = + abi.encodePacked("GET /oauth2/v3/certs2 HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length)); + _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + } + + /// Only the origin-form line, like every other verifier. + function test_refusesAnAbsoluteFormRequestTarget() public { + bytes memory request = abi.encodePacked( + "GET https://www.googleapis.com/oauth2/v3/certs HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length) + ); + _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + } + + /// googleapis.com answers for many hosts under one certificate. With two + /// `Host` headers, which backend answered is the backend's business. + function test_refusesASecondHostHeader() public { + bytes memory request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", + "host: www.googleapis.com\r\n", + "host: storage.googleapis.com\r\n", + "connection: close\r\n\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 2)); + } + + /// Field names are case-insensitive and the colon admits whitespace, so + /// the count runs over normalized bytes. + function test_refusesASecondHostHeaderEvadingCaseAndWhitespace() public { + bytes memory request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", + "host: www.googleapis.com\r\n", + "HOST\t : storage.googleapis.com\r\n", + "connection: close\r\n\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 2)); + } + + function test_refusesARequestWithoutAHostHeader() public { + bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nconnection: close\r\n\r\n"; + _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 0)); + } + + /// The TLS authority is the certificate's name; the `Host` header is + /// which of Google's backends answered. Both must be Google's JWKS host. + function test_refusesAnotherGoogleHost() public { + bytes memory request = + "GET /oauth2/v3/certs HTTP/1.1\r\nhost: storage.googleapis.com\r\nconnection: close\r\n\r\n"; + _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + } + + /// A host that is a prefix of the right one, or the right one followed by + /// more, is another host. + function test_refusesAHostThatMerelyContainsTheRightOne() public { + bytes memory request = + "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com.evil\r\nconnection: close\r\n\r\n"; + _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.co\r\nconnection: close\r\n\r\n"; + _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + } + + // ─── What it refuses: the response ────────────────────────────── + + function _withResponse(bytes memory response) private view returns (bytes memory) { + return _attest(REQUEST, response, _now()); + } + + /// Only the server's agreement counts. A redirect carries a body too, and + /// it is not the key set. + function test_refusesARedirect() public { + bytes memory response = + abi.encodePacked("HTTP/1.1 302 Found\r\nlocation: /elsewhere\r\ncontent-length: 0\r\n\r\n"); + _refuses(_withResponse(response), IdentityJwksRoots.WrongStatusLine.selector); + } + + function test_refusesAResponseWithoutAHeadBoundary() public { + bytes memory response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "content-length: 2\r\n", "{}"); + _refuses(_withResponse(response), IdentityJwksRoots.NoHeadBoundary.selector); + } + + /// Every deviation from strict chunked framing is one error: a size that + /// is not hex, a chunk extension, a missing terminator, trailers, an + /// over-long size, and chunk data not followed by CRLF. + function test_refusesBadChunkFraming() public { + bytes memory head = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "transfer-encoding: chunked\r\n\r\n"); + bytes memory body = _oneKey("kid-1", "one"); + bytes memory sizeLine = abi.encodePacked(_hex(body.length), "\r\n"); + + // A size that is not hex. + _refuses( + _withResponse(abi.encodePacked(head, "zz\r\n", body, "\r\n0\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // A chunk extension. + _refuses( + _withResponse(abi.encodePacked(head, _hex(body.length), ";ext=1\r\n", body, "\r\n0\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // No terminating zero chunk. + _refuses( + _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n")), IdentityJwksRoots.BadChunkFraming.selector + ); + // Trailers after the zero chunk. + _refuses( + _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\nx-trailer: 1\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // Nine hex digits. + _refuses( + _withResponse(abi.encodePacked(head, "000000001\r\n", body, "\r\n0\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // Chunk data not followed by CRLF. + _refuses( + _withResponse(abi.encodePacked(head, sizeLine, body, "0\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // A size past the end of the body. + _refuses( + _withResponse(abi.encodePacked(head, "ffff\r\n", body, "\r\n0\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // Bytes after the final CRLF. + _refuses( + _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\n\r\n\r\n")), + IdentityJwksRoots.BadChunkFraming.selector + ); + // And the strict form of the same body is accepted. + _rotate(_withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\n\r\n"))); + } + + function test_refusesAContentLengthThatIsNotTheBody() public { + bytes memory body = _oneKey("kid-1", "one"); + bytes memory response = abi.encodePacked( + STATUS_OK, CONTENT_TYPE, "content-length: ", vm.toString(body.length + 1), "\r\n\r\n", body + ); + _refuses( + _withResponse(response), + abi.encodeWithSelector(IdentityJwksRoots.ContentLengthMismatch.selector, body.length + 1, body.length) + ); + + // A value that is not a decimal declares no length any body matches. + response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "content-length: 4x\r\n\r\n", body); + _refuses( + _withResponse(response), + abi.encodeWithSelector(IdentityJwksRoots.ContentLengthMismatch.selector, 4, body.length) + ); + } + + /// With neither framing header, `connection: close` delimits and the body + /// is what follows the head. + function test_acceptsABodyDelimitedByConnectionClose() public { + bytes memory response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "\r\n", _oneKey("kid-1", "one")); + _rotate(_withResponse(response)); + assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); + } + + // ─── What it refuses: the key set ─────────────────────────────── + + function _withBody(bytes memory body) private view returns (bytes memory) { + return _reading(body); + } + + function test_refusesABodyWithoutKeys() public { + _refuses( + _withBody(abi.encodePacked('{"kyes":[', _jwk("kid-1", _modulus("one")), "]}")), + abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "keys") + ); + // Present, but not an array. + _refuses( + _withBody(abi.encodePacked('{"keys":', _jwk("kid-1", _modulus("one")), "}")), + abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "keys") + ); + } + + /// A decoy: two `keys` members, and nothing to say which one Google's + /// parser -- or a verifier's -- would honour. + function test_refusesADecoyKeysMember() public { + bytes memory body = abi.encodePacked( + '{"keys":[', _jwk("kid-1", _modulus("one")), '],"keys":[', _jwk("kid-2", _modulus("two")), "]}" + ); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.AmbiguousMember.selector, "keys")); + } + + function test_refusesAKeyWithoutAModulus() public { + bytes memory body = _body('{"kty":"RSA","kid":"kid-1","e":"AQAB"}'); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "n")); + } + + function test_refusesAKeyWithoutAKid() public { + bytes memory body = _body(abi.encodePacked('{"kty":"RSA","n":"', _modulus("one"), '","e":"AQAB"}')); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "kid")); + } + + /// Two `kid` members in one object: a duplicate leaves the value + /// undefined, and different parsers pick different ones. + function test_refusesADuplicateKidMember() public { + bytes memory body = _body(abi.encodePacked('{"kid":"kid-1","kid":"kid-2","n":"', _modulus("one"), '"}')); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.AmbiguousMember.selector, "kid")); + } + + function test_refusesAnUnterminatedKid() public { + bytes memory body = _body(abi.encodePacked('{"n":"', _modulus("one"), '","kid":"kid-1}')); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "kid")); + } + + function test_refusesAnUnterminatedKeysArray() public { + _refuses( + _withBody(abi.encodePacked('{"keys":[', _jwk("kid-1", _modulus("one")))), + abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + ); + // An element that is not an object. + _refuses( + _withBody(abi.encodePacked('{"keys":[', _jwk("kid-1", _modulus("one")), ',"x"]}')), + abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + ); + // An object with no closing brace. + _refuses( + _withBody(abi.encodePacked('{"keys":[{"kid":"kid-1","n":"', _modulus("one"), '"')), + abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + ); + } + + /// A backslash is refused rather than unescaped: a kid and a base64url + /// modulus need none, and unescaping is a parser this contract does not + /// carry. + function test_refusesAnEscapedValue() public { + bytes memory body = _body(abi.encodePacked('{"kid":"kid\\"1","n":"', _modulus("one"), '"}')); + _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.EscapedValue.selector, "kid")); + } + + function test_refusesANestedObject() public { + bytes memory body = _body(abi.encodePacked('{"kid":"kid-1","x5c":["MIIC"],"n":"', _modulus("one"), '"}')); + _refuses(_withBody(body), IdentityJwksRoots.UnexpectedNesting.selector); + body = _body(abi.encodePacked('{"kid":"kid-1","inner":{},"n":"', _modulus("one"), '"}')); + _refuses(_withBody(body), IdentityJwksRoots.UnexpectedNesting.selector); + } + + /// The cap on tracked kids is also the cap on keys in one reading. + function test_refusesSeventeenKeys() public { + _refuses(_withBody(_manyKeys(17)), IdentityJwksRoots.TooManyKids.selector); + } + + function test_refusesTrailingBytes() public { + bytes memory body = _oneKey("kid-1", "one"); + _refuses(_withBody(abi.encodePacked(body, "x")), IdentityJwksRoots.TrailingBytes.selector); + _refuses(_withBody(abi.encodePacked(body, "{}")), IdentityJwksRoots.TrailingBytes.selector); + // No closing brace at all. + _refuses(_withBody(_slice(body, 0, body.length - 1)), IdentityJwksRoots.TrailingBytes.selector); + // But whitespace after it is what Google sends. + _rotate(_withBody(abi.encodePacked(body, "\n"))); + } + + /// The circuit takes a 2048-bit modulus and nothing else. + function test_refusesAShortModulus() public { + bytes memory body = _body(_jwk("kid-1", string(_b64url(_bytes("short", 255))))); + _refuses(_withBody(body), IdentityJwksRoots.InvalidModulusLength.selector); + } + + function test_refusesANonBase64urlCharacter() public { + bytes memory body = _body(_jwk("kid-1", "AAA+")); + _refuses(_withBody(body), IdentityJwksRoots.InvalidB64Char.selector); + } + + function test_refusesAnImpossibleBase64Length() public { + bytes memory body = _body(_jwk("kid-1", "AAAAA")); + _refuses(_withBody(body), IdentityJwksRoots.InvalidB64Length.selector); + } + + // ─── Helpers ──────────────────────────────────────────────────── + + /// Read through the cheatcode rather than `block.timestamp`: under via-IR + /// the optimizer treats TIMESTAMP as movable and may re-read it after a + /// `vm.warp`, so a value captured before the warp is not one. + function _now() private view returns (uint64) { + return uint64(vm.getBlockTimestamp()); + } + + function _slice(bytes memory data, uint256 from, uint256 to) private pure returns (bytes memory out) { + out = new bytes(to - from); + for (uint256 i = 0; i < out.length; ++i) { + out[i] = data[from + i]; + } + } } diff --git a/solidity/contracts/identity/test/fixtures/google-jwks.json b/solidity/contracts/identity/test/fixtures/google-jwks.json new file mode 100644 index 00000000..fab49cfe --- /dev/null +++ b/solidity/contracts/identity/test/fixtures/google-jwks.json @@ -0,0 +1,20 @@ +{ + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "use": "sig", + "kid": "943a3a5d7d919625a454e489b75c29adab57acba", + "n": "pIpnzA2ezyEERJSxiqpLBmMeIqATH-V6iuBtKIibXEyYovujrx8niqTeO6RIyXT6uDUUv0V2kJ8V_iWYFxzXY1BqK9IfcAmjg0XUDoyTVkoyLsF0gj299LH-zw5vCvy8jmamFIZKAbKcQ5hpHvSittM1vl-6vVL-i2GxyGbMA9aY6Hq15NylS1t7ELTYfQimlnvxcb7_DM0cuS5U1SfbCZMCpKhh0nrSlYds240oxpCJOV2rBahs_Ea5c7tezS1nwVC9W_E-bR9TF6BHkC_fv-E8DcWfkI_6geaJzBhINNxBfjx-w1-WUp2Jz3YYFWEfeQjxMqu-Fg6cGxwk7V16uQ", + "kty": "RSA" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "f10f87405a979c1df36df26606734f33cd85c271", + "alg": "RS256", + "n": "4rY5uwZK1dQ-UVgB5s4NLyC-u5LC2MT7b8GWZztiNgMsp0Nnqx0pM7Ofx0ws32N2aZcx10-J8ydQxnNb9uAcf-7LyhyOIcv_WEyzaSbUAMOgoF-nQmJetckxNg6ekhNfaFcTQS0T-29ql2_CBLIML6CvSh-r0fgWRsqN2ayB7wCl74Gv6OOVbvagUWhj5z2L6o_plmsPDwLVuvA7o3WDEDjoq-IXafRQowj92kQUenrOKD4YCopuLIBhel6VH8doFRNZ6KISQhMcOivWaLU_UtKKAMloGJieTf_3r-_nErs2h5wB7T7FrMCScmO7mvFQXKh8_4P-MlbfgS9CUvQksw" + } + ] +} diff --git a/solidity/contracts/identity/test/fixtures/oidc/jwks-proof.json b/solidity/contracts/identity/test/fixtures/oidc/jwks-proof.json deleted file mode 100644 index 891e1bbd..00000000 --- a/solidity/contracts/identity/test/fixtures/oidc/jwks-proof.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "notary_signature": "0xac2dd7d49bc1d763ebec55bf3ea8d4ca3057a6c990a84658cbbc6bda6c6f8fc62b8bf782ecc81fdbe17a49a9900c5ce8599d99bdd4bdba199d98a5d63d3ec7d21c", - "domain_hash": "0x59962e64d51a4d36e444d18d029aff438ce3ddb882eb151a0af0fdde3cb655ee", - "client_random": "0x0000000000000000000000000000000000000000000000000000000000000000", - "server_random": "0x0000000000000000000000000000000000000000000000000000000000000000", - "server_ephemeral_key": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transcript_root": "0xb66b4209a1c2e90f9bd4318986e32bffce84ba9cadd2690426dc0e19e78924c3", - "timestamp": 1777900352, - "domain_path": [ - "0x3a48ec7938d008c4fbdfc033b9c0ada126a46089e460e5f3af5eda1352f9a4ee", - "0x4fd66d65911c483bedf4d98f568b8f40ce2cc7b652a9cf849b269f07de10c1cd" - ], - "endpoint_path": [ - "0x2fc77341079e14a1103642484ef49d836a16a456f497722239924495e377d4a8", - "0x4fd66d65911c483bedf4d98f568b8f40ce2cc7b652a9cf849b269f07de10c1cd" - ], - "claims": [ - { - "kid": "19caaecde8f485e8f5938f48aba0ce7a3581f027", - "n_b64url": "xGoOLta_JnpIStUy0UMlbQRnMZhRZ3MJqm3TQMb2nXQwstM91mEhjjGikBJoCA4sGXyq9SQmLcicQ-ezhnTD5TCqbLhjz_NndK4IUUaUcG0eOxYMnG0LlLJhZ4BonBjA7m-xDDL-vO7wSVi0t9xbYi4abO48NqJQn65rlE-mhIc15tgLawIm9oq7yoI1jYYsn3Ik_PMp-h8W-CHiUSxDhYfkIuMtnMoBwpjGLiWjzlV-yBNce1V1iKRK7uIuyuypAL3SKAcHCfxQEfAFBUuf3pM2qGcaBCtPpW096QaXdTs8_Tjoq7DwsmaPLOtK5EKzkDUcp0noRlDWB6fPP-rUMw", - "jwk_bytes": "0x7b0a2020202020202265223a202241514142222c0a202020202020226b7479223a2022525341222c0a20202020202022616c67223a20225253323536222c0a20202020202022757365223a2022736967222c0a202020202020226e223a202278476f4f4c74615f4a6e70495374557930554d6c6251526e4d5a68525a334d4a716d3354514d62326e58517773744d39316d45686a6a47696b424a6f43413473475879713953516d4c636963512d657a686e544435544371624c686a7a5f4e6e644b344955556155634730654f78594d6e47304c6c4c4a685a34426f6e426a41376d2d7844444c2d764f3777535669307439786259693461624f34384e714a516e3635726c452d6d684963313574674c6177496d396f7137796f49316a5959736e33496b5f504d702d6838572d434869555378446859666b49754d746e4d6f4277706a474c69576a7a6c562d79424e6365315631694b524b3775497579757970414c33534b41634843667851456641464255756633704d3271476361424374507057303936516158645473385f546a6f71374477736d61504c4f744b35454b7a6b44556370306e6f526c445742366650502d72554d77222c0a202020202020226b6964223a202231396361616563646538663438356538663539333866343861626130636537613335383166303237220a202020207d", - "jwk_path": [ - "0xc757d6ad13f0c4383803188f05d92c1386c91ee1b378bfd61107e8dcd6c73e58", - "0x546e7f882e139fc1436c0b1a28055a8c72d534e32529335e9297875dc7ab05ae" - ] - }, - { - "kid": "8076ddbab415556e169154c6a4a0bdbd4469b79c", - "n_b64url": "uPQl9hY_shV81dJgVp7qJdGcOsEkLD5g6EdpBOuzzul4THxflhFAkzJACPUSUdQpOIw5pKHD9sGhnLoqnOu-n_AdbJtAI_NG56KWawoZY1YQRr6GqJxccMTeh2vEdaM6haiYKbftbCk7RWsvWia0gPhiIfC6_D3OwI6ZVIsMp-U9fTDBo2wEG5fA4DIZn_QU8TAyzi4BJbgQYXmy3LETW-90v5S3ub7WFsaGqL7NEdxER_RQ6m7L2Pl4jIjbN8bkNKeUslXYm56MH1x3hD5RqeKJoZylpgT00XVHs4br3p1iSz4t8uPvvy0Q2oNEVkXgv36i7QK4s_oe_qhZFfW_UQ", - "jwk_bytes": "0x7b0a202020202020226b7479223a2022525341222c0a2020202020202265223a202241514142222c0a20202020202022757365223a2022736967222c0a20202020202022616c67223a20225253323536222c0a202020202020226e223a20227550516c3968595f7368563831644a67567037714a6447634f73456b4c44356736456470424f757a7a756c34544878666c6846416b7a4a4143505553556451704f497735704b4844397347686e4c6f716e4f752d6e5f4164624a7441495f4e4735364b5761776f5a5931595152723647714a7863634d54656832764564614d36686169594b62667462436b37525773765769613067506869496643365f44334f7749365a5649734d702d5539665444426f327745473566413444495a6e5f5155385441797a6934424a62675159586d79334c4554572d3930763553337562375746736147714c374e45647845525f5251366d374c32506c346a496a624e38626b4e4b6555736c58596d35364d483178336844355271654b4a6f5a796c70675430305856487334627233703169537a34743875507676793051326f4e45566b58677633366937514b34735f6f655f71685a4666575f5551222c0a202020202020226b6964223a202238303736646462616234313535353665313639313534633661346130626462643434363962373963220a202020207d", - "jwk_path": [ - "0x5faffac008736e1a823a01a782add8c6911e47106674955396c01954f5f7e301", - "0x546e7f882e139fc1436c0b1a28055a8c72d534e32529335e9297875dc7ab05ae" - ] - } - ] -} \ No newline at end of file diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 16dc29d0..cbd92504 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -22,12 +22,9 @@ fs_permissions = [ # self-skip when these files are absent. { access = "read", path = "circuits/jwt_email/target" }, { access = "read", path = "contracts/login/test/fixtures/oidc" }, - # The naming system keeps its own copy: the rotation proof binds no - # contract, so the same notarized reading feeds both trust lists, but the - # identity tree must not read out of the login tree. - { access = "read", path = "contracts/identity/test/fixtures/oidc" }, - # TS/Solidity parity fixture for bind-request encoding (vendored from the - # browser identity package; see BindEncoding.t.sol). + # Google's real JWKS body, as served today, so the root list's parser is + # tested against the document it will actually read rather than a shape + # a test author remembered (see IdentityJwksRoots.t.sol). { access = "read", path = "contracts/identity/test/fixtures" }, { access = "read", path = "out" }, # diamond deployer reads facet methodIdentifiers ] diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index 2bbc5bbb..cd092973 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -15,6 +15,7 @@ import {XZkVerifier, IHonkVerifier} from "../contracts/login/zk/XZkVerifier.sol" import {IZkSessionVerifier} from "../contracts/login/zk/IZkSessionVerifier.sol"; import {IdentityNames} from "../contracts/identity/IdentityNames.sol"; import {NotaryService} from "../contracts/ceremony/NotaryService.sol"; +import {INotaryService} from "../contracts/ceremony/INotaryService.sol"; import {CeremonyProofVerifier} from "../contracts/ceremony/CeremonyProofVerifier.sol"; import {IProofVerifier} from "../contracts/ceremony/IProofVerifier.sol"; import {HandleVectors} from "../contracts/identity/HandleVectors.sol"; @@ -185,11 +186,13 @@ contract Deploy is Script, BankDiamondDeployer { // The JWKS root list is the naming system's own. Google's Platform // Verifier reads the trusted moduli through it, and nothing is trusted - // until a notarized reading of Google's JWKS lands. + // until a notarized reading of Google's JWKS lands -- verified through + // the Notary Service above, like every other notarized session. IdentityJwksRoots rootsImpl = new IdentityJwksRoots(); address jwksRootsAddr = address( new ERC1967Proxy( - address(rootsImpl), abi.encodeCall(IdentityJwksRoots.initialize, (deployer, address(notaryContract))) + address(rootsImpl), + abi.encodeCall(IdentityJwksRoots.initialize, (deployer, INotaryService(notaryServiceAddr))) ) ); From 2b784b94f593c0d6c7dfd45fe266bd792019c27e Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 02:48:42 +0100 Subject: [PATCH 02/15] refactor!: archive the wallet products; crate and package cover the identity stack The login (Registry, WebWallet, WalletFactory, the X ZK and Google OIDC verifiers) and transfer (Bank diamond) products, and the legacy Notary they verified through, move to solidity/archived/ -- outside every forge root, so nothing compiles, tests or ships them. They stay in the tree rather than only in history because they are the worked example of a full product built on notarized sessions; archived/README.md says so and pins where they last built (v0.6.0). Why now: IdentityJwksRoots was the last consumer of the legacy Notary, and it now reads a notarized session through NotaryService like every other contract. With that, nothing under contracts/ referenced the wallet products, and keeping them compiled meant keeping two bb-generated verifiers reproducible against a circuits release the ceremony circuits are replacing. So the regeneration machinery goes with them: regen-verifiers.sh, its dispatch workflow, CI's verifiers job (and publish-crates' needs on it) and the circuits-version pin. The ceremony circuits bring their own regeneration when their release lands; git history keeps the script. Deploy.s.sol deploys the identity stack only, in dependency order: NotaryService -> CeremonyProofVerifier -> IdentityNames (a keyspace per platform) -> IdentityJwksRoots pointed at the Notary Service. It reads DEPLOYER_KEY/PRIVATE_KEY, NOTARY_ADDRESS and NOTARY_FEE_WEI; BACKEND_ADDRESS and X_CLIENT_ID are gone. deploy.yml never named any of the removed ones. The Rust crate and the npm package follow. The login/notary/oidc/transfer bindings and the diamond helpers are deleted; a ceremony module binds NotaryService and CeremonyProofVerifier; IdentityJwksRoots' binding is the rotate(bytes,bytes) payable interface with quoteRotation and notaryService; and the covered-artifact lists (COVERED, vendor-artifacts.sh, codegen.mjs) name the seven contracts that remain. load_linked_bytecode stays although nothing links a library today: the UltraHonk verifiers the ceremony circuits bring do, and this is the path they will deploy through. The anvil tests deploy the identity stack behind proxies and read the wiring back, rotate and upgrade the Notary Service, and bootstrap the factory with a NotaryService proxy; the TypeScript payable example is a rotation paying the Notary Fee. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .github/workflows/ci.yml | 56 +-- .github/workflows/regen-verifiers.yml | 86 ----- .gitignore | 2 - README.md | 27 +- rust/contracts/Cargo.toml | 2 +- rust/contracts/README.md | 89 +++-- rust/contracts/src/artifacts.rs | 126 +------ rust/contracts/src/bindings/ceremony.rs | 92 +++++ rust/contracts/src/bindings/identity.rs | 59 +-- rust/contracts/src/bindings/login.rs | 321 ----------------- rust/contracts/src/bindings/mod.rs | 5 +- rust/contracts/src/bindings/notary.rs | 30 -- rust/contracts/src/bindings/oidc.rs | 80 ----- rust/contracts/src/bindings/transfer.rs | 231 ------------ rust/contracts/src/deploy.rs | 11 +- rust/contracts/src/diamond.rs | 181 ---------- rust/contracts/src/lib.rs | 20 +- rust/contracts/tests/anvil.rs | 336 +++++++----------- scripts/regen-verifiers.sh | 162 --------- scripts/vendor-artifacts.sh | 32 +- solidity/archived/README.md | 45 +++ .../login/IRegistry.sol | 0 .../login/Registry.sol | 0 .../login/SnapshotProxy.sol | 0 .../login/WalletFactory.sol | 0 .../login/WebWallet.sol | 0 .../login/oidc/GoogleOidcVerifier.sol | 0 .../login/oidc/IOidcVerifier.sol | 0 .../login/oidc/Verifier.sol | 0 .../login/oidc/script/DeployOidc.s.sol | 0 .../login/test/Registry.t.sol | 0 .../login/test/TlsLinkGriefPoC.t.sol | 0 .../login/test/WalletFactory.t.sol | 0 .../login/test/WebWallet.t.sol | 0 .../login/test/fixtures/oidc/jwks-proof.json | 0 .../test/fixtures/oidc/jwks-snapshot.json | 0 .../test/oidc/GoogleOidcVerifierFlow.t.sol | 0 .../login/test/oidc/README.md | 0 .../test/oidc/RegistryOidcLinkFlow.t.sol | 0 .../login/zk/IZkSessionVerifier.sol | 0 .../login/zk/XHonkVerifier.sol | 0 .../login/zk/XZkVerifier.sol | 0 .../login/zk/test/XZkVerifierAllowlist.t.sol | 0 .../login/zk/test/XZkVerifierBindings.t.sol | 0 .../login/zk/test/XZkVerifierFlow.t.sol | 0 .../notary/INotary.sol | 0 .../{contracts => archived}/notary/Notary.sol | 0 .../notary/test/DeployNotary.sol | 0 .../notary/test/Notary.t.sol | 0 .../transfer/MockERC20.sol | 0 .../transfer/bank/BankErrors.sol | 0 .../transfer/bank/BankInit.sol | 0 .../transfer/bank/BankModifiers.sol | 0 .../transfer/bank/BankTypes.sol | 0 .../transfer/bank/IBank.sol | 0 .../transfer/bank/facets/AdminFacet.sol | 0 .../transfer/bank/facets/TransferFacet.sol | 0 .../transfer/bank/facets/VaultFacet.sol | 0 .../transfer/bank/libraries/LibEscrow.sol | 0 .../transfer/bank/libraries/LibProof.sol | 0 .../transfer/bank/libraries/LibRecipient.sol | 0 .../transfer/bank/libraries/LibString.sol | 0 .../transfer/bank/libraries/LibTemplate.sol | 0 .../transfer/bank/storage/LibCoreStorage.sol | 0 .../bank/storage/LibEscrowStorage.sol | 0 .../bank/storage/LibTemplateStorage.sol | 0 .../transfer/bank/storage/LibTokenStorage.sol | 0 .../transfer/diamond/Diamond.sol | 0 .../diamond/facets/DiamondCutFacet.sol | 0 .../diamond/facets/DiamondLoupeFacet.sol | 0 .../diamond/facets/OwnershipFacet.sol | 0 .../diamond/interfaces/IDiamondCut.sol | 0 .../diamond/interfaces/IDiamondLoupe.sol | 0 .../transfer/diamond/interfaces/IERC173.sol | 0 .../transfer/diamond/libraries/LibDiamond.sol | 0 .../transfer/script/BankDiamondDeployer.sol | 0 .../transfer/script/DeployBankDiamond.s.sol | 0 .../transfer/test/BalanceViews.t.sol | 0 .../transfer/test/BankDiamond.t.sol | 0 .../transfer/test/BankDiamondHonor.t.sol | 0 .../transfer/test/DollarToken.t.sol | 0 .../transfer/test/EdgeCases.t.sol | 0 .../transfer/test/EscrowById.t.sol | 0 .../transfer/test/ImplicitTemplate.t.sol | 0 .../transfer/test/Integration.t.sol | 0 .../test/IntegrationNewFeatures.t.sol | 0 .../transfer/test/LibStringGuards.t.sol | 0 .../transfer/test/NativeAsset.t.sol | 0 .../transfer/test/PausableFlow.t.sol | 0 .../transfer/test/RegistryBankRecycle.t.sol | 0 .../transfer/test/TemplateMatching.t.sol | 0 .../transfer/test/TransferWithin.t.sol | 0 solidity/circuits-version | 1 - solidity/contracts/WTIA9.sol | 6 +- .../contracts/factory/test/LibidFactory.t.sol | 18 +- solidity/contracts/identity/IdentityNames.sol | 4 +- solidity/foundry.toml | 13 +- solidity/script/Deploy.s.sol | 190 +++------- ts/packages/contracts/README.md | 25 +- ts/packages/contracts/scripts/codegen.mjs | 21 +- ts/packages/contracts/src/calls.test.ts | 29 +- 101 files changed, 481 insertions(+), 1819 deletions(-) delete mode 100644 .github/workflows/regen-verifiers.yml create mode 100644 rust/contracts/src/bindings/ceremony.rs delete mode 100644 rust/contracts/src/bindings/login.rs delete mode 100644 rust/contracts/src/bindings/notary.rs delete mode 100644 rust/contracts/src/bindings/oidc.rs delete mode 100644 rust/contracts/src/bindings/transfer.rs delete mode 100644 rust/contracts/src/diamond.rs delete mode 100755 scripts/regen-verifiers.sh create mode 100644 solidity/archived/README.md rename solidity/{contracts => archived}/login/IRegistry.sol (100%) rename solidity/{contracts => archived}/login/Registry.sol (100%) rename solidity/{contracts => archived}/login/SnapshotProxy.sol (100%) rename solidity/{contracts => archived}/login/WalletFactory.sol (100%) rename solidity/{contracts => archived}/login/WebWallet.sol (100%) rename solidity/{contracts => archived}/login/oidc/GoogleOidcVerifier.sol (100%) rename solidity/{contracts => archived}/login/oidc/IOidcVerifier.sol (100%) rename solidity/{contracts => archived}/login/oidc/Verifier.sol (100%) rename solidity/{contracts => archived}/login/oidc/script/DeployOidc.s.sol (100%) rename solidity/{contracts => archived}/login/test/Registry.t.sol (100%) rename solidity/{contracts => archived}/login/test/TlsLinkGriefPoC.t.sol (100%) rename solidity/{contracts => archived}/login/test/WalletFactory.t.sol (100%) rename solidity/{contracts => archived}/login/test/WebWallet.t.sol (100%) rename solidity/{contracts => archived}/login/test/fixtures/oidc/jwks-proof.json (100%) rename solidity/{contracts => archived}/login/test/fixtures/oidc/jwks-snapshot.json (100%) rename solidity/{contracts => archived}/login/test/oidc/GoogleOidcVerifierFlow.t.sol (100%) rename solidity/{contracts => archived}/login/test/oidc/README.md (100%) rename solidity/{contracts => archived}/login/test/oidc/RegistryOidcLinkFlow.t.sol (100%) rename solidity/{contracts => archived}/login/zk/IZkSessionVerifier.sol (100%) rename solidity/{contracts => archived}/login/zk/XHonkVerifier.sol (100%) rename solidity/{contracts => archived}/login/zk/XZkVerifier.sol (100%) rename solidity/{contracts => archived}/login/zk/test/XZkVerifierAllowlist.t.sol (100%) rename solidity/{contracts => archived}/login/zk/test/XZkVerifierBindings.t.sol (100%) rename solidity/{contracts => archived}/login/zk/test/XZkVerifierFlow.t.sol (100%) rename solidity/{contracts => archived}/notary/INotary.sol (100%) rename solidity/{contracts => archived}/notary/Notary.sol (100%) rename solidity/{contracts => archived}/notary/test/DeployNotary.sol (100%) rename solidity/{contracts => archived}/notary/test/Notary.t.sol (100%) rename solidity/{contracts => archived}/transfer/MockERC20.sol (100%) rename solidity/{contracts => archived}/transfer/bank/BankErrors.sol (100%) rename solidity/{contracts => archived}/transfer/bank/BankInit.sol (100%) rename solidity/{contracts => archived}/transfer/bank/BankModifiers.sol (100%) rename solidity/{contracts => archived}/transfer/bank/BankTypes.sol (100%) rename solidity/{contracts => archived}/transfer/bank/IBank.sol (100%) rename solidity/{contracts => archived}/transfer/bank/facets/AdminFacet.sol (100%) rename solidity/{contracts => archived}/transfer/bank/facets/TransferFacet.sol (100%) rename solidity/{contracts => archived}/transfer/bank/facets/VaultFacet.sol (100%) rename solidity/{contracts => archived}/transfer/bank/libraries/LibEscrow.sol (100%) rename solidity/{contracts => archived}/transfer/bank/libraries/LibProof.sol (100%) rename solidity/{contracts => archived}/transfer/bank/libraries/LibRecipient.sol (100%) rename solidity/{contracts => archived}/transfer/bank/libraries/LibString.sol (100%) rename solidity/{contracts => archived}/transfer/bank/libraries/LibTemplate.sol (100%) rename solidity/{contracts => archived}/transfer/bank/storage/LibCoreStorage.sol (100%) rename solidity/{contracts => archived}/transfer/bank/storage/LibEscrowStorage.sol (100%) rename solidity/{contracts => archived}/transfer/bank/storage/LibTemplateStorage.sol (100%) rename solidity/{contracts => archived}/transfer/bank/storage/LibTokenStorage.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/Diamond.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/facets/DiamondCutFacet.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/facets/DiamondLoupeFacet.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/facets/OwnershipFacet.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/interfaces/IDiamondCut.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/interfaces/IDiamondLoupe.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/interfaces/IERC173.sol (100%) rename solidity/{contracts => archived}/transfer/diamond/libraries/LibDiamond.sol (100%) rename solidity/{contracts => archived}/transfer/script/BankDiamondDeployer.sol (100%) rename solidity/{contracts => archived}/transfer/script/DeployBankDiamond.s.sol (100%) rename solidity/{contracts => archived}/transfer/test/BalanceViews.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/BankDiamond.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/BankDiamondHonor.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/DollarToken.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/EdgeCases.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/EscrowById.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/ImplicitTemplate.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/Integration.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/IntegrationNewFeatures.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/LibStringGuards.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/NativeAsset.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/PausableFlow.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/RegistryBankRecycle.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/TemplateMatching.t.sol (100%) rename solidity/{contracts => archived}/transfer/test/TransferWithin.t.sol (100%) delete mode 100644 solidity/circuits-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c89a0976..24b17713 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,60 +106,6 @@ jobs: - name: Generated files are current run: python3 scripts/regen-identity-handles.py --check - # --------------------------------------------------------------------------- - # Verifiers: the committed bb-generated Solidity verifiers reproduce from - # the pinned libid-circuits release. This replaces the reproducibility gate - # that died with the monorepo split — there, the deploy workflow - # recompiled the circuit next to the contracts; here, the circuits live in - # libid-circuits and this job re-derives both verifiers from that repo's - # released verification keys (verifier ⟵ vk alone, no nargo needed) and - # byte-compares against the committed sources. A mismatch means the - # deployed verifier no longer corresponds to the circuit the release - # ships — the web prover and the on-chain verifier must roll together. - # - # solidity/circuits-version pins the release tag. Until that release is - # actually cut on libid-org/libid-circuits, the download step fails with an - # explicit message — that is intended, not flaky. - # - # The download/verify/regenerate/compare chain is scripts/regen-verifiers.sh - # (--check here); the "Regenerate verifiers" dispatch workflow runs the same - # script with --write to refresh the committed files after a pin bump. - # - # publish-crates hangs its `needs:` off this job so nothing publishes over a - # drifted verifier; publish-npm inherits the gate through publish-crates. - # --------------------------------------------------------------------------- - verifiers: - name: Verifiers match circuits release - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - # Keyed on the pin file, not the manifest's bb version — the manifest is - # only known after the download, which now happens inside the script. A - # pin bump starts a fresh cache; the script reuses a cached bb when its - # version matches the new manifest and installs via bbup otherwise. - - name: Cache barretenberg - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.bb - key: bb-${{ runner.os }}-circuits-${{ hashFiles('solidity/circuits-version') }} - - # Same forge as everywhere else in this workflow, binary only — fmt - # reads foundry.toml but compiles nothing. - - uses: foundry-rs/foundry-toolchain@908c540300062bd5a7e473851cdb4282204cee09 # v1.9.1 - with: - version: ${{ env.FOUNDRY_VERSION }} - - # The whole chain — download the pinned release, verify sha256s, install - # bb at the manifest's version, regenerate with post-processing, - # byte-compare — lives in the script, shared with the "Regenerate - # verifiers" dispatch workflow so the check and the writer cannot drift. - - name: Regenerate verifiers and compare - env: - GH_TOKEN: ${{ github.token }} - run: ./scripts/regen-verifiers.sh --check - # --------------------------------------------------------------------------- # Rust: vendored artifacts, formatting, clippy, tests. Needs forge twice # over — the vendored artifacts are generated from a fresh `forge build`, @@ -454,7 +400,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 if: github.event_name == 'release' && github.event.action == 'published' - needs: [verify-tag, versions, solidity, rust, publish-check, verifiers] + needs: [verify-tag, versions, solidity, rust, publish-check] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/regen-verifiers.yml b/.github/workflows/regen-verifiers.yml deleted file mode 100644 index 03eb8352..00000000 --- a/.github/workflows/regen-verifiers.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Regenerate verifiers - -# Manually dispatched companion to CI's `verifiers` check. After bumping -# solidity/circuits-version on a branch, run this action on that branch (the -# UI's branch selector picks the ref): it runs scripts/regen-verifiers.sh -# --write to regenerate both committed verifiers from the newly pinned -# libid-circuits release, rebuilds the contracts, re-vendors the Rust -# artifacts (they embed the verifier bytecode, so a vk change changes them — -# the ts ABIs are interface-only and unaffected), and pushes the result back -# to the same branch as one signed-off bot commit. If nothing changed, it -# says so and exits green. -# -# Every third-party action is pinned by commit SHA, with the tag in a -# comment, so a moved tag cannot change what executes. - -on: - workflow_dispatch: - -# One writer per branch: two dispatches against the same ref queue instead of -# racing each other's push. -concurrency: - group: regen-verifiers-${{ github.ref }} - cancel-in-progress: false - -permissions: - contents: write - -env: - # Keep in sync with FOUNDRY_VERSION in ci.yml — the regenerated files must - # format and build under the same forge the CI check uses. - FOUNDRY_VERSION: v1.7.1 - -jobs: - regen: - name: Regenerate verifiers and push - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - # persist-credentials so the push step below can use the workflow token; - # full history so the branch push is a plain fast-forward regardless of - # where the ref sits. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: true - submodules: recursive - - # Same key scheme as ci.yml's verifiers job: keyed on the pin file, and - # the script reuses a cached bb only when its version matches the - # manifest. - - name: Cache barretenberg - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.bb - key: bb-${{ runner.os }}-circuits-${{ hashFiles('solidity/circuits-version') }} - - # Installs forge (and warms the shared artifact cache) via the same - # composite the CI jobs use; the post-regeneration build below then only - # recompiles what the new verifiers touched. - - name: Build contracts - uses: ./.github/actions/forge-build - with: - foundry-version: ${{ env.FOUNDRY_VERSION }} - - - name: Regenerate verifiers from the pinned circuits release - env: - GH_TOKEN: ${{ github.token }} - run: ./scripts/regen-verifiers.sh --write - - - name: Commit and push if anything changed - run: | - set -euo pipefail - if [ -z "$(git status --porcelain)" ]; then - echo "::notice::Verifiers already match the pinned circuits release; nothing to commit." - exit 0 - fi - tag="$(cat solidity/circuits-version)" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git status --short - # The Signed-off-by trailer is what the DCO job checks for. - git commit \ - -m "chore: regenerate ZK verifiers for $tag" \ - -m "Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" - git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" diff --git a/.gitignore b/.gitignore index e924c361..356143ca 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ solidity/broadcast/ ts/node_modules/ ts/packages/*/node_modules/ ts/packages/*/dist/ -solidity/.circuits-check/ -circuits-release/ # Generated from `forge build`, never committed. Both are packaged into their # published artefact (the crate embeds artifacts/ via include_dir!, the npm diff --git a/README.md b/README.md index 7d221d92..28608f70 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ # libID contracts Smart contracts for libID, laid out per chain. `solidity/` is a self-contained -Foundry project holding the EVM contracts (login wallets, transfer bank -diamond, identity naming); room is reserved for `solana/` and other networks, -and `rust/` and `ts/` ABI wrapper packages are coming. +Foundry project holding the EVM contracts: the ceremony verification path +(Notary Service, Proof Verifier, Platform Verifiers), the identity naming +system with its Google JWKS root list, and the deterministic deployment +factory. `rust/` and `ts/` hold the ABI wrapper packages; room is reserved +for `solana/` and other networks. ## Layout ``` solidity/ # Foundry project root contracts/ - login/ # registry, web wallets, OIDC + ZK session verifiers - transfer/ # bank diamond and facets - identity/ # platform handle naming and identity verifiers + ceremony/ # NotaryService, CeremonyProofVerifier, Platform Verifiers + identity/ # IdentityNames, IdentityJwksRoots, handle normalization + factory/ # LibidFactory: deterministic CREATE3 deployment WTIA9.sol # wrapped TIA + archived/ # the frozen wallet and bank products: reference only, + # never compiled (see archived/README.md) script/Deploy.s.sol lib/ # git submodules (openzeppelin, forge-std) +rust/contracts/ # libid-contracts crate: alloy bindings + embedded artifacts +ts/packages/contracts/ # @libid/contracts: viem ABIs, call builders, identity helpers scripts/ + vendor-artifacts.sh regen-identity-handles.py ``` @@ -45,10 +52,6 @@ CI runs both in every job that compiles the crate or the package, and again in the publish jobs — the published crate and npm package carry the generated output even though git does not. -Some login OIDC flow tests read locally generated proof artifacts from -`circuits/jwt_email/target/`; when those files are absent the tests skip -themselves — that is expected. - ## Handle vectors `solidity/contracts/identity/handles.json` is the source of truth for platform @@ -91,10 +94,6 @@ Publishing the release triggers CI's release jobs: provenance. A prerelease publishes under its first prerelease identifier as the dist-tag (`1.2.0-rc.1` → `rc`); a plain version under `latest`. -To pick up a new circuits release: bump `solidity/circuits-version` on a -branch, run the "Regenerate verifiers" action on that branch, then review the -bot commit and merge — CI's verifiers job re-proves the regeneration. - ## License Dual-licensed under MIT and Apache-2.0; see `LICENSE-MIT`, `LICENSE-APACHE` diff --git a/rust/contracts/Cargo.toml b/rust/contracts/Cargo.toml index f9c4e712..638542a2 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 contract stack (login, transfer bank diamond, identity names)." +description = "Typed alloy bindings, embedded forge artifacts, and deploy/upgrade helpers for the libid identity stack (Notary Service, ceremony proof verifier, identity names, Google JWKS roots, deterministic factory)." keywords = ["ethereum", "alloy", "contracts", "deployment"] categories = ["cryptography::cryptocurrencies"] # The vendored artifacts are part of the crate: consumers deploy from the diff --git a/rust/contracts/README.md b/rust/contracts/README.md index 203dc0a1..ea723941 100644 --- a/rust/contracts/README.md +++ b/rust/contracts/README.md @@ -1,9 +1,11 @@ # libid-contracts Typed [alloy](https://github.com/alloy-rs/alloy) bindings, embedded forge -artifacts, and deploy/upgrade helpers for the libid contract stack: the login -contracts (`Registry`, `WebWallet`, `WalletFactory`, verifiers), the Bank -EIP-2535 diamond, and the identity-names contracts. +artifacts, and deploy/upgrade helpers for the libid identity stack: the +ceremony verification path (`NotaryService`, `CeremonyProofVerifier`), the +naming system (`IdentityNames`) with its Google JWKS trust list +(`IdentityJwksRoots`), 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 @@ -11,17 +13,17 @@ dependencies at runtime**. They are generated, not committed: `scripts/vendor-artifacts.sh` produces `artifacts/` from `solidity/` and CI runs it before every build, test and publish. Working in this repo, run it once after cloning — the crate embeds the directory with `include_dir!`, so -until it exists `cargo build` fails at macro expansion. Signing stays on the consumer's -side: every helper is generic over an alloy `Provider` you have already wired -with a wallet. +until it exists `cargo build` fails at macro expansion. Signing stays on the +consumer's side: every helper is generic over an alloy `Provider` you have +already wired with a wallet. -## Example: deploy the login stack +## Example: deploy the Notary Service and the JWKS root list ```rust,no_run -use alloy::{primitives::Address, providers::ProviderBuilder}; +use alloy::{primitives::{Address, U256}, providers::ProviderBuilder}; use libid_contracts::{ - bindings::login::{IRegistryAdmin, Registry, WalletFactory}, - deploy::{deploy_behind_proxy, deploy_contract}, + bindings::{ceremony::NotaryService, identity::IdentityJwksRoots}, + deploy::deploy_behind_proxy, Artifacts, }; @@ -32,69 +34,64 @@ async fn main() -> Result<(), Box> { .wallet(/* your signer */ todo!()) .connect_http("https://rpc.example.org".parse()?); let artifacts = Artifacts::embedded(); - let (owner, notary, backend): (Address, Address, Address) = todo!(); + let (owner, notary_key): (Address, Address) = todo!(); - // WebWallet implementation, then WalletFactory + Registry behind - // ERC1967 proxies. - let wallet_impl = deploy_contract( - &provider, - artifacts.bytecode("WebWallet")?, - "WebWallet (impl)", - ) - .await?; - let factory = deploy_behind_proxy( + // The Notary Service first: every notarized session is verified through + // it, and it charges the Notary Fee for doing so. + let notary = deploy_behind_proxy( &provider, &artifacts, - "WalletFactory", - &WalletFactory::initializeCall { + "NotaryService", + &NotaryService::initializeCall { owner_: owner, - walletImpl_: wallet_impl, - registry_: Address::ZERO, + notary_: notary_key, + fee_: U256::from(1_000), }, None, ) .await?; - let registry = deploy_behind_proxy( + + // The JWKS root list pays that fee on every rotation. It starts empty: + // a keeper submits a notarized reading of Google's JWKS to seed it. + let roots = deploy_behind_proxy( &provider, &artifacts, - "Registry", - &IRegistryAdmin::initializeCall { - _notary: notary, - _backend: backend, - _walletFactory: factory, - _owner: owner, + "IdentityJwksRoots", + &IdentityJwksRoots::initializeCall { + owner_: owner, + notary_: notary, }, None, ) .await?; - WalletFactory::new(factory, &provider) - .setRegistry(registry) - .send() - .await? - .get_receipt() - .await?; - println!("registry {registry:#x}, resolves via {:#x}", registry); + let fee = IdentityJwksRoots::new(roots, &provider) + .quoteRotation() + .call() + .await?; + println!("roots {roots:#x} verify through {notary:#x}; a rotation costs {fee} wei"); Ok(()) } ``` Other entry points: -- `diamond::deploy_bank_diamond` / `diamond::replace_bank_facets` — the Bank - EIP-2535 deploy and facet upgrade. -- `deploy::load_linked_bytecode` (or `Artifacts::linked_bytecode`) — deploys - and links external libraries (`ZKTranscriptLib` for the generated UltraHonk - verifiers) before returning the creation bytecode. - `deploy::upgrade_uups` — deploy a fresh implementation and `upgradeToAndCall` a UUPS proxy onto it. -- `Artifacts::facet_selectors` / `Artifacts::method_identifiers` — selector - extraction from the vendored `methodIdentifiers`. +- `factory::ensure_factory` / `factory::factory_deploy` — install the + canonical cross-network factory where missing and deploy protocol proxies + through it at name-derived CREATE3 addresses. +- `deploy::load_linked_bytecode` (or `Artifacts::linked_bytecode`) — deploys + and links external libraries before returning the creation bytecode. Nothing + covered today links one; the UltraHonk verifiers the ceremony circuits bring + will. +- `Artifacts::method_identifiers` — selector extraction from the vendored + `methodIdentifiers`. ## Testing Unit tests run everywhere; the integration tests in `tests/anvil.rs` spawn -`anvil` (foundry) and deploy every stack for real. From `rust/`: +`anvil` (foundry) and deploy the stack for real. From `rust/`: ```sh cargo +nightly fmt --all # nightly only — stable rustfmt mangles imports diff --git a/rust/contracts/src/artifacts.rs b/rust/contracts/src/artifacts.rs index 0a2fde19..65c76347 100644 --- a/rust/contracts/src/artifacts.rs +++ b/rust/contracts/src/artifacts.rs @@ -14,10 +14,7 @@ use std::{ use alloy::{ hex, - primitives::{ - Bytes, - FixedBytes, - }, + primitives::Bytes, providers::Provider, }; use include_dir::{ @@ -34,38 +31,19 @@ use crate::error::{ static EMBEDDED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/artifacts"); /// Every deployable contract the crate covers, as `(file, contract)` — the -/// artifact lives at `.sol/.json`. Interfaces (`IBank`) are -/// vendored for their `methodIdentifiers` but carry no bytecode, so they are -/// not listed here. Keep in sync with `scripts/vendor-artifacts.sh`. +/// artifact lives at `.sol/.json`. Keep in sync with +/// `scripts/vendor-artifacts.sh`. pub const COVERED: &[(&str, &str)] = &[ - // login - ("Registry", "Registry"), - ("Notary", "Notary"), - ("WalletFactory", "WalletFactory"), - ("WebWallet", "WebWallet"), + // ceremony + ("NotaryService", "NotaryService"), + ("CeremonyProofVerifier", "CeremonyProofVerifier"), ("ERC1967Proxy", "ERC1967Proxy"), - ("XZkVerifier", "XZkVerifier"), - ("XHonkVerifier", "XHonkVerifier"), - // oidc — the Google OIDC circuit verifier is generated into `Verifier.sol` - // under the contract name `HonkVerifier` - ("Verifier", "HonkVerifier"), - ("GoogleOidcVerifier", "GoogleOidcVerifier"), - // transfer (bank diamond) - ("Diamond", "Diamond"), - ("DiamondCutFacet", "DiamondCutFacet"), - ("DiamondLoupeFacet", "DiamondLoupeFacet"), - ("OwnershipFacet", "OwnershipFacet"), - ("AdminFacet", "AdminFacet"), - ("VaultFacet", "VaultFacet"), - ("TransferFacet", "TransferFacet"), - ("BankInit", "BankInit"), - ("MockERC20", "MockERC20"), - ("WTIA9", "WTIA9"), // identity ("IdentityNames", "IdentityNames"), ("IdentityJwksRoots", "IdentityJwksRoots"), // factory ("LibidFactory", "LibidFactory"), + ("WTIA9", "WTIA9"), ]; enum Source { @@ -126,7 +104,7 @@ impl Artifacts { } /// Creation bytecode where the source file and contract names differ - /// (`Verifier.sol` → `HonkVerifier`). + /// (a bb-generated `Verifier.sol` holding `HonkVerifier`, say). pub fn bytecode_named(&self, file: &str, contract: &str) -> Result { let hex_str = self.bytecode_hex(file, contract)?; if hex_str.contains("__$") { @@ -145,9 +123,11 @@ impl Artifacts { /// Creation bytecode with every external library it references deployed /// (recursively) through `provider` and linked in. Mirrors what forge does - /// automatically: the generated UltraHonk verifiers link `ZKTranscriptLib`. - /// For artifacts with no link references this behaves like - /// [`Self::bytecode_named`] (no transaction is sent). + /// automatically. Nothing covered today links a library; the UltraHonk + /// verifiers the ceremony circuits bring link `ZKTranscriptLib`, and this + /// is the path they will deploy through. For artifacts with no link + /// references this behaves like [`Self::bytecode_named`] (no transaction + /// is sent). /// /// `sender` opts into explicit nonce management (see /// [`deploy_contract_from`](crate::deploy::deploy_contract_from)). @@ -184,27 +164,6 @@ impl Artifacts { .collect() } - /// All external function selectors of a facet, decoded from its - /// `methodIdentifiers`. Mirrors `BankDiamondDeployer._selectors`. - pub fn facet_selectors(&self, contract: &str) -> Result>> { - let methods = self.method_identifiers(contract)?; - let mut selectors = Vec::with_capacity(methods.len()); - for (sig, hex_sel) in &methods { - let bytes = hex::decode(hex_sel).map_err(|e| Error::Artifact { - detail: format!("invalid selector hex for {sig}: {e}"), - })?; - let arr: [u8; 4] = - bytes.as_slice().try_into().map_err(|_| Error::Artifact { - detail: format!( - "selector for {sig} is {} bytes, expected 4", - bytes.len() - ), - })?; - selectors.push(FixedBytes::<4>::from(arr)); - } - Ok(selectors) - } - /// The raw `bytecode.object` hex (no `0x`), link placeholders intact. pub(crate) fn bytecode_hex(&self, file: &str, contract: &str) -> Result { let json = self.raw(file, contract)?; @@ -241,8 +200,8 @@ mod tests { use super::*; /// Every covered contract's creation bytecode is present and non-empty. - /// The Honk verifiers carry link placeholders, so only the hex is checked - /// here; linking is exercised by the anvil tests. + /// Only the hex is checked here so a future artifact with link + /// placeholders still passes; linking is the anvil tests' business. #[test] fn every_covered_contract_has_bytecode() { let artifacts = Artifacts::embedded(); @@ -257,7 +216,9 @@ mod tests { } } - /// Contracts without link references decode straight to bytes. + /// Contracts without link references decode straight to bytes — which is + /// every covered contract today, so this doubles as the check that none of + /// them silently grew a library dependency the vendor script must follow. #[test] fn unlinked_contracts_decode() { let artifacts = Artifacts::embedded(); @@ -274,55 +235,4 @@ mod tests { } } } - - /// The Honk verifiers link `ZKTranscriptLib`, and the library artifact - /// they resolve against is vendored alongside them. - #[test] - fn honk_verifiers_link_the_transcript_lib() { - let artifacts = Artifacts::embedded(); - for (file, contract) in [ - ("XHonkVerifier", "XHonkVerifier"), - ("Verifier", "HonkVerifier"), - ] { - let refs = artifacts.link_references(file, contract).unwrap(); - assert!(!refs.is_empty(), "{contract} should carry link references"); - for (lib_path, libs) in &refs { - let stem = std::path::Path::new(lib_path) - .file_stem() - .and_then(|s| s.to_str()) - .unwrap(); - for lib_name in libs.as_object().unwrap().keys() { - let lib_hex = artifacts.bytecode_hex(stem, lib_name).unwrap(); - assert!(!lib_hex.is_empty(), "{lib_name} library missing"); - } - } - } - } - - /// Facet selector extraction: every Bank facet exposes selectors, they are - /// 4 bytes each, and none repeats across facets (a diamondCut would revert - /// on a duplicate). - #[test] - fn facet_selectors_extract_and_are_disjoint() { - let artifacts = Artifacts::embedded(); - let mut all = std::collections::BTreeSet::new(); - for facet in [ - "DiamondCutFacet", - "DiamondLoupeFacet", - "OwnershipFacet", - "AdminFacet", - "VaultFacet", - "TransferFacet", - ] { - let selectors = artifacts.facet_selectors(facet).unwrap(); - assert!(!selectors.is_empty(), "{facet} has no selectors"); - for sel in selectors { - assert!(all.insert(sel), "duplicate selector {sel} in {facet}"); - } - } - // diamondCut(FacetCut[],address,bytes) — the one selector the Diamond - // constructor wires in itself. - let cut = artifacts.method_identifiers("DiamondCutFacet").unwrap(); - assert!(cut.keys().any(|sig| sig.starts_with("diamondCut("))); - } } diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs new file mode 100644 index 00000000..f159d0d7 --- /dev/null +++ b/rust/contracts/src/bindings/ceremony.rs @@ -0,0 +1,92 @@ +//! Bindings for the ceremony verification path (`solidity/contracts/ceremony/`): +//! the Notary Service every notarized session is authenticated through, and +//! the Proof Verifier that routes a claim to the Platform Verifier registered +//! for its version. + +/// Bindings for `ceremony/NotaryService.sol` (which implements +/// `INotaryService`). +/// +/// Authenticates one attestation and charges one fee for it. The digest is +/// derived from the attested bytes on chain, never taken from the caller +/// (REQ-COMMON-49), which is why `verify` is not on this interface: a +/// consumer contract calls it with the fee attached, and the decoded record +/// comes back to that contract, not to an off-chain reader. What an operator +/// does from here is hold the trusted key set, set the fee and withdraw what +/// accrued. +#[allow(clippy::too_many_arguments, unused_attributes)] +mod notary_service_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface NotaryService { + /// `notary_` is the first trusted key; `fee_` may be zero (a + /// deployment may meter at no charge, and the exact-value rule + /// still applies). + function initialize(address owner_, address notary_, uint256 fee_) external; + /// What one verification costs, in the chain's native asset. + /// Readable before a submission is built, so it can be bounded. + function fee() external view returns (uint256); + function setFee(uint256 fee_) external; + /// Add or remove a trusted notary key. Several are held at once so + /// a rotation can overlap: add the incoming key, remove the + /// outgoing one once nothing can still present under it. + function setNotary(address key, bool trusted_) external; + function isTrustedNotary(address key) external view returns (bool); + /// Fees accrue here rather than being forwarded per verification. + function withdraw(address to, uint256 amount) external; + + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event FeeChanged(uint256 previousFee, uint256 newFee); + event NotaryTrustChanged(address indexed key, bool trusted); + event FeesWithdrawn(address indexed to, uint256 amount); + } + } +} + +pub use notary_service_inner::NotaryService; + +/// Bindings for `ceremony/CeremonyProofVerifier.sol` (which implements +/// `IProofVerifier`). +/// +/// The Supported Version Set: which Platform Verifier answers for a +/// `(platformId, verifierVersion)` pair. Governance registers one with +/// `setVerifier`; `IdentityNames.claim` dispatches through `verify`, which is +/// not on this interface for the same reason `NotaryService.verify` is not — +/// it is called by the consumer contract with the fee attached. +#[allow(clippy::too_many_arguments, unused_attributes)] +mod proof_verifier_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface CeremonyProofVerifier { + function initialize(address owner_) external; + /// Register (or, with the zero address, remove) the Platform + /// Verifier for a pair. The verifier must serve `platformId`. + function setVerifier(bytes32 platformId, uint16 verifierVersion, address verifier) external; + /// The Platform Verifier registered for a pair, or zero. + function verifierOf(bytes32 platformId, uint16 verifierVersion) external view returns (address); + /// What one claim under this pair costs: the registered + /// verifier's quote, forwarded whole. + function quote(bytes32 platformId, uint16 verifierVersion) external view returns (uint256); + /// Whether any version is registered for the platform at all. + function verifiesPlatform(bytes32 platformId) external view returns (bool); + /// This chain's identifier, as the digest construction takes it. + function chainId() external view returns (bytes32); + + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event VerifierConfigured(bytes32 indexed platformId, uint16 indexed verifierVersion, address verifier); + } + } +} + +pub use proof_verifier_inner::CeremonyProofVerifier; diff --git a/rust/contracts/src/bindings/identity.rs b/rust/contracts/src/bindings/identity.rs index 4fb50570..06d8f056 100644 --- a/rust/contracts/src/bindings/identity.rs +++ b/rust/contracts/src/bindings/identity.rs @@ -1,6 +1,5 @@ //! Bindings for the identity-names stack (`solidity/contracts/identity/`): -//! `IdentityNames`, the per-platform verifiers, and the Google JWKS trust -//! list. +//! `IdentityNames` and its Google JWKS trust list. /// Bindings for `identity/IdentityNames.sol`. /// @@ -88,6 +87,12 @@ pub use names_inner::IdentityNames; /// Bindings for `identity/IdentityJwksRoots.sol` — the naming system's own /// Google JWKS trust list. Starts EMPTY: Google names bind only once a /// notarized reading of Google's JWKS has landed here. +/// +/// A rotation is an ordinary notarized session: the keeper reveals the whole +/// `GET /oauth2/v3/certs` exchange, `rotate` hands the attested bytes and the +/// notary's proof to the Notary Service with the Notary Fee attached (read +/// it with `quoteRotation`, which is the service's `fee()`), and the contract +/// reads the JWKS out of the transcript it vouched for. #[allow(clippy::too_many_arguments, unused_attributes)] mod jwks_roots_inner { use alloy::sol; @@ -95,27 +100,6 @@ mod jwks_roots_inner { sol! { #[sol(rpc)] interface IdentityJwksRoots { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct NotarizedJwksProof { - bytes notarySignature; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] endpointPath; - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct JwkClaim { - bytes jwkBytes; - bytes32[] jwkPath; - bytes kid; - bytes nB64url; - } - #[derive(Debug, serde::Serialize, serde::Deserialize)] struct RootInfo { bytes32 kidHash; @@ -124,17 +108,36 @@ mod jwks_roots_inner { uint256 expiresAt; } - function initialize(address owner_, address notaryContract_) external; + function initialize(address owner_, address notary_) external; + /// The Notary Service a rotation is verified through. + function notaryService() external view returns (address); + function setNotaryService(address notary_) external; + /// What one rotation costs beyond gas: the Notary Fee, forwarded + /// whole. `rotate` must be sent with exactly this value. + function quoteRotation() external view returns (uint256); + /// Permissionless. `attestedData` is the ceremony-common section + /// 9.1 record of the JWKS session, `proof` the notary's + /// authentication of it (a 65-byte EIP-191 signature today). + function rotate(bytes calldata attestedData, bytes calldata proof) external payable; function untrustModulus(bytes32 modulusHash) external; - function rotate(NotarizedJwksProof calldata proof, JwkClaim[] calldata claims) external; function prune() external; + + function modulusOfKid(bytes32 kidHash) external view returns (bytes32); + function expiresAtKid(bytes32 kidHash) external view returns (uint256); + /// What `GooglePlatformVerifier` reads: modulus hash -> expiry, + /// zero when untrusted. function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); - function notaryContract() external view returns (address); - function notary() external view returns (address); - function currentRoots() external view returns (RootInfo[] memory); + function rotatedAtKid(bytes32 kidHash) external view returns (uint256); function freshestObservedAt() external view returns (uint256); + function currentRoots() external view returns (RootInfo[] memory); function needsRotation() external view returns (bool); + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event NotaryServiceChanged(address notary); event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); event ModulusUntrusted(bytes32 indexed modulusHash); event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); diff --git a/rust/contracts/src/bindings/login.rs b/rust/contracts/src/bindings/login.rs deleted file mode 100644 index f667dc39..00000000 --- a/rust/contracts/src/bindings/login.rs +++ /dev/null @@ -1,321 +0,0 @@ -//! Bindings for the login stack: `Registry`, `WebWallet`, `WalletFactory`, -//! and the X ZK verifier. Mirrors `solidity/contracts/login/`. - -/// Bindings for `login/Registry.sol`. -/// -/// `register_session` has many parameters by design (matching the Solidity -/// contract interface), so the too_many_arguments lint is suppressed. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod registry_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface Registry { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct FullTlsProof { - bytes notarySignature; - bytes backendSignature; - address userAddress; - address walletAddress; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] usernamePath; - bytes32[] endpointPath; - bytes32[] idPath; - } - - function register_session( - FullTlsProof calldata proof, - string calldata domain, - string calldata username, - string calldata userId, - string calldata endpoint - ) external; - - /// OIDC variant — caller provides ABI-encoded `UserProof` bytes - /// understood by the configured `IOidcVerifier` for `platform`. - function register_session_oidc( - string calldata platform, - bytes calldata oidcProof - ) external; - - function register_session_zk( - string calldata platform, - bytes calldata zkProof - ) external; - - function link_identity_zk( - string calldata platform, - bytes calldata zkProof - ) external; - - function link_identity_oidc( - string calldata platform, - bytes calldata oidcProof, - address sessionAddr - ) external; - - function linkIdentity( - string calldata platform, - string calldata handle, - string calldata userId, - FullTlsProof calldata proof, - string calldata endpoint - ) external; - - function resolve(string calldata platform, string calldata handle) external view returns (address); - function resolveById(string calldata platform, string calldata id) external view returns (address); - function handleHint(string calldata platform, string calldata handle) external view returns (string memory); - function getHandles(address wallet) external view returns (string[] memory platforms, string[] memory handles); - function getCurrentHandles(address wallet) external view returns (string[] memory platforms, string[] memory handles); - function getUserIds(address wallet) external view returns (string[] memory platforms, string[] memory userIds); - function getPlatform(string calldata domain) external view returns (string memory endpoint, string memory handlePrefix); - function zkVerifierOf(string calldata domain) external view returns (address); - function oidcVerifierOf(string calldata platform) external view returns (address); - /// The shared Notary contract the Registry routes attestation checks through. - function notaryContract() external view returns (address); - /// The current notary signer, read through the Notary contract. - function notary() external view returns (address); - function backend() external view returns (address); - function walletFactory() external view returns (address); - - event HandleRegistered(string platform, string handle, address indexed owner); - event SessionRegistered(string platform, string handle, address indexed wallet, address sessionAddr); - event IdentityLinked(string platform, string handle, address indexed wallet); - event HandleChanged(string platform, string userId, string handle); - } - } -} - -pub use registry_inner::Registry; - -/// Owner/deploy-time surface of the Registry: `initialize` (ABI-encoded into -/// the ERC1967 proxy init data) and configuration setters. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod registry_admin_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IRegistryAdmin { - function initialize(address _notaryContract, address _backend, address _walletFactory, address _owner) external; - function setPlatform( - string calldata domain, - string calldata endpoint, - string calldata handlePrefix, - string calldata idPrefix, - string calldata idSuffix - ) external; - function removePlatform(string calldata domain) external; - function setZkVerifier(string calldata domain, address verifier) external; - function setOidcVerifier(string calldata platform, address verifier) external; - function setBackend(address _backend) external; - function pause() external; - function unpause() external; - } - } -} - -pub use registry_admin_inner::IRegistryAdmin; - -/// Bindings for `login/zk/XZkVerifier.sol`. -/// -/// The Registry calls this verifier through `IZkSessionVerifier`; a consumer -/// uses these bindings to ABI-encode `XProof` (the opaque payload it forwards -/// on `register_session_zk(platform, bytes)`) and to verify proofs via -/// `eth_call` in pre-flight. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod x_zk_verifier_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface XZkVerifier { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct TokenAttestation { - bytes32 bearerHash; - uint32 bearerRangeStart; - uint32 bearerRangeEnd; - bytes sentRevealed; - uint64 timestamp; - bytes notarySignature; - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct MeAttestation { - bytes32 bearerHash; - uint32 bearerRangeStart; - uint32 bearerRangeEnd; - bytes sentRevealed; - uint32 sentPrefixEnd; - uint32 sentSuffixEnd; - bytes recvRevealed; - string handle; - string userId; - address sessionAddr; - uint64 timestamp; - bytes notarySignature; - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct XProof { - bytes proof; - bytes32[] publicInputs; - TokenAttestation tokenAttest; - MeAttestation meAttest; - } - - function initialize( - address _owner, - address _notaryContract, - address _honkVerifier, - bytes calldata _xClientId, - string calldata _endpoint, - string calldata _handlePrefix, - string calldata _platformName - ) external; - - function verifyAndExtract(bytes calldata payload) - external view - returns ( - string memory handle, - address sessionKey, - address walletAddress, - uint256 expiresAt, - bytes32 nullifier, - string memory userId - ); - - function platformName() external view returns (string memory); - function notaryContract() external view returns (address); - function notary() external view returns (address); - - /// client_id allowlist. An EMPTY allowlist denies every client_id; - /// `openClientIds` is the explicit opt-in to accept any app. - function isClientIdAllowed(bytes32 clientIdHash) external view returns (bool); - function openClientIds() external view returns (bool); - function clientIdCount() external view returns (uint256); - function clientIdAt(uint256 index) external view returns (bytes memory); - - function addClientId(bytes calldata _id) external; - function removeClientId(bytes calldata _id) external; - function setOpenClientIds(bool _open) external; - - event ClientIdAdded(bytes clientId); - event ClientIdRemoved(bytes clientId); - event OpenClientIdsSet(bool open); - } - } -} - -pub use x_zk_verifier_inner::XZkVerifier; - -/// Bindings for `login/WebWallet.sol`. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod wallet_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface WebWallet { - function threshold() external view returns (uint8); - function nonce() external view returns (uint256); - function registry() external view returns (address); - function factory() external view returns (address); - function identityCount() external view returns (uint256); - function identityAt(uint256 index) external view returns (bytes32); - function isIdentity(bytes32 identityHash) external view returns (bool); - function sessionCount(bytes32 platformHandleKey) external view returns (uint256); - function sessionAt(bytes32 platformHandleKey, uint256 index) external view returns (address); - function isSession(address addr) external view returns (bool); - function sessionIdentity(address addr) external view returns (bytes32); - function execute( - address target, - uint256 value, - bytes calldata data, - bytes[] calldata sigs - ) external; - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata datas, - bytes[] calldata sigs - ) external; - function upgradeToLatest() external; - function setThreshold(uint8 newThreshold) external; - - event SessionRegistered(string platform, string handle, address indexed sessionAddr); - event SessionRevoked(string platform, string handle, address indexed sessionAddr); - event Executed(address indexed target, uint256 value, uint256 nonce); - event NativeReceived(address indexed from, uint256 amount); - event NativeSent(address indexed to, uint256 amount); - } - } -} - -pub use wallet_inner::WebWallet; - -/// Bindings for `login/WalletFactory.sol`. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod factory_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface WalletFactory { - function initialize(address owner_, address walletImpl_, address registry_) external; - function setRegistry(address newRegistry) external; - function latestImplementation() external view returns (address); - function upgradeWalletImplementation(address newImplementation) external; - - event WalletCreated(address indexed wallet, bytes32 indexed firstIdentity); - event WalletImplementationUpgraded(address indexed newImplementation); - } - } -} - -pub use factory_inner::WalletFactory; - -/// Minimal ERC-20 surface (transfer event scanning + balances). -#[allow(clippy::too_many_arguments, unused_attributes)] -mod erc20_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IERC20 { - function balanceOf(address account) external view returns (uint256); - function transfer(address to, uint256 value) external returns (bool); - function approve(address spender, uint256 value) external returns (bool); - function allowance(address owner, address spender) external view returns (uint256); - - event Transfer(address indexed from, address indexed to, uint256 value); - event Approval(address indexed owner, address indexed spender, uint256 value); - } - } -} - -pub use erc20_inner::IERC20; - -/// UltraHonk verifier interface — matches the `IHonkVerifier` the login and -/// identity verifiers call. Used off-chain in pre-flight to bounce bogus proof -/// bytes before paying gas. View call only; no state mutation. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod honk_verifier_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IHonkVerifier { - function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool); - } - } -} - -pub use honk_verifier_inner::IHonkVerifier; diff --git a/rust/contracts/src/bindings/mod.rs b/rust/contracts/src/bindings/mod.rs index f7a82ba4..0de60553 100644 --- a/rust/contracts/src/bindings/mod.rs +++ b/rust/contracts/src/bindings/mod.rs @@ -1,10 +1,7 @@ //! Hand-written `alloy::sol!` bindings, kept in lockstep with the Solidity //! sources in `solidity/contracts`. Grouped by product area. +pub mod ceremony; pub mod factory; pub mod identity; -pub mod login; -pub mod notary; -pub mod oidc; pub mod proxy; -pub mod transfer; diff --git a/rust/contracts/src/bindings/notary.rs b/rust/contracts/src/bindings/notary.rs deleted file mode 100644 index d46dfb51..00000000 --- a/rust/contracts/src/bindings/notary.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Bindings for the shared notary-attestation verifier. Mirrors -//! `solidity/contracts/notary/`. - -/// Bindings for `notary/Notary.sol` (which implements `INotary`). -/// -/// Every other contract holds a pointer to this proxy and calls -/// `verify(digest, proof)`. V1 checks a secp256k1 signature over the -/// EIP-191-prefixed digest against a single stored signer; rotation is -/// `setNotary`, and a change of proof system is a UUPS upgrade of this proxy -/// alone. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod notary_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface Notary { - function initialize(address owner_, address notary_) external; - function setNotary(address notary_) external; - function notary() external view returns (address); - /// True when `proof` is a valid notary attestation of `digest`. - /// Answers false (never reverts) on a malformed proof. - function verify(bytes32 digest, bytes calldata proof) external view returns (bool); - - event NotaryChanged(address indexed previousNotary, address indexed newNotary); - } - } -} - -pub use notary_inner::Notary; diff --git a/rust/contracts/src/bindings/oidc.rs b/rust/contracts/src/bindings/oidc.rs deleted file mode 100644 index 14021f51..00000000 --- a/rust/contracts/src/bindings/oidc.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Bindings for the Google OIDC verifier -//! (`solidity/contracts/login/oidc/GoogleOidcVerifier.sol`). - -/// The surface a deployer and a JWKS rotation listener need. Keep the struct -/// fields in lockstep with the Solidity types. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface GoogleOidcVerifier { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct NotarizedJwksProof { - bytes notarySignature; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] endpointPath; - } - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct JwkClaim { - bytes jwkBytes; - bytes32[] jwkPath; - bytes kid; - bytes nB64url; - } - - /// `_verifier` is the deployed Honk circuit verifier - /// (`Verifier.sol:HonkVerifier`); `notaryContract_` is the shared - /// Notary contract; `initialAud` seeds the audience allowlist — - /// without it the verifier is fail-closed and rejects every proof. - function initialize( - address _verifier, - address _owner, - address notaryContract_, - string calldata initialAud - ) external; - - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct RootInfo { - bytes32 kidHash; - bytes32 modulusHash; - uint256 observedAt; - uint256 expiresAt; - } - - function setExpectedAudience(string calldata clientId) external; - function setExpectedAudienceHash(bytes32 audienceHash) external; - function setRegistry(address r) external; - function untrustModulus(bytes32 modulusHash) external; - - function modulusOfKid(bytes32 kidHash) external view returns (bytes32); - function expiresAtKid(bytes32 kidHash) external view returns (uint256); - function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); - function platformName() external view returns (string memory); - function notaryContract() external view returns (address); - function notary() external view returns (address); - function currentRoots() external view returns (RootInfo[] memory); - function freshestObservedAt() external view returns (uint256); - function needsRotation() external view returns (bool); - - function rotate(NotarizedJwksProof calldata proof, JwkClaim[] calldata claims) external; - function prune() external; - - event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); - event ModulusUntrusted(bytes32 indexed modulusHash); - event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); - event AudienceConfigured(bytes32 indexed audienceHash, string clientId); - } - } -} - -pub use inner::GoogleOidcVerifier; diff --git a/rust/contracts/src/bindings/transfer.rs b/rust/contracts/src/bindings/transfer.rs deleted file mode 100644 index e4896ee3..00000000 --- a/rust/contracts/src/bindings/transfer.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Bindings for the Bank diamond (`solidity/contracts/transfer/`): the full -//! `IBank` surface addressed through the diamond, plus the EIP-2535 cut/loupe -//! interfaces and the one-shot `BankInit`. - -/// The Bank diamond's external surface (Vault + Admin + Transfer facets), for -/// callers that address the diamond as one contract. Mirrors -/// `transfer/bank/IBank.sol` and the facet sources. -/// -/// `balanceOf` and `balanceOfTotal` are overloaded exactly as in Solidity; -/// alloy disambiguates the generated Rust methods as `balanceOf_0` -/// (address-keyed) and `balanceOf_1` (identity-keyed), in declaration order. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod bank_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface Bank { - struct NotaryTlsProof { - bytes notarySig; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] bodyMerklePath; - bytes32[] authorMerklePath; - bytes32[] domainMerklePath; - bytes32[] endpointMerklePath; - bytes32[] authorIdMerklePath; - bytes32[] receiverIdMerklePath; - bytes revealedReceiverId; - bytes32[] quotedRefMerklePath; - bytes revealedQuotedRef; - bytes32[] quotedAuthorMerklePath; - bytes revealedQuotedAuthorId; - } - - struct ResourceInfo { - string platform; // API domain (e.g. "api.x.com") - string resourceType; - string resourceId; - string requestPath; - } - - struct SenderInfo { - string author; - bytes revealedAuthor; - bytes revealedAuthorId; - } - - struct BackendSig { - bytes sig; - uint256 timestamp; - } - - // ── Transfer facet ─────────────────────────────────────────── - - /// Token-by-name overload. - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - string calldata tokenName_, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external; - - /// Token-by-address overload. - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external; - - /// Stateless preflight of a TLS-notarized comment proof. - function verifyProof( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverUserId - ) external view returns (bool); - - function transfer_within( - string calldata receiverPlatform, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - uint256 amount - ) external; - - // ── Vault facet ────────────────────────────────────────────── - - function deposit(string calldata platform, string calldata handle, string calldata userId, address token, uint256 amount) external payable; - function withdraw(address recipient, address token, uint256 amount) external; - function balanceOf(address wallet, address token) external view returns (uint256); - function balanceOf(string calldata platform, string calldata handle, string calldata userId, address token) external view returns (uint256); - function balanceOfTotal(address wallet, address token) external view returns (uint256); - function balanceOfTotal(string calldata platform, string calldata handle, string calldata userId, address token) external view returns (uint256); - function registeredBalances(address wallet, address token) external view returns (uint256); - function unregisteredBalances(bytes32 key, address token) external view returns (uint256); - function identityHash(string calldata domain, string calldata username) external pure returns (bytes32); - - // ── Admin facet ────────────────────────────────────────────── - - function pause() external; - function unpause() external; - function setRegistry(address _registry) external; - function registerToken(string calldata name, address token) external; - function unregisterToken(string calldata name) external; - function resolveToken(string calldata name) external view returns (address); - function getRegisteredTokens() external view returns (address[] memory); - function tokenName(address token) external view returns (string memory); - function setPlatformTemplate(string calldata platform, string calldata newTemplate) external; - function clearPlatformTemplates(string calldata platform) external; - function platformTemplateCount(string calldata platform) external view returns (uint256); - function getPlatformTemplate(string calldata platform, uint256 index) external view returns (string memory); - function setResourceTypePrefix(string calldata resourceType, string calldata prefix) external; - function setPlatformWebPrefix(string calldata platform, string calldata prefix) external; - function getPlatformWebPrefix(string calldata platform) external view returns (string memory); - function registry() external view returns (address); - function paused() external view returns (bool); - - // ── Events ─────────────────────────────────────────────────── - - event Deposited(address indexed wallet, bytes32 indexed handleKey, address indexed token, uint256 amount); - event Withdrawn(address indexed wallet, address indexed recipient, address indexed token, uint256 amount); - event TransferWithin(address indexed sender, string receiverPlatform, string receiverHandle, string receiverUserId, address indexed token, uint256 amount); - event WebTransferV3(address indexed senderWallet, address indexed receiverWallet, string receiverPlatform, string receiverHandle, string receiverUserId, address indexed token, uint256 amount, string sourceUrl); - event Paused(address account); - event Unpaused(address account); - event RegistrySet(address indexed oldRegistry, address indexed newRegistry); - event TokenRegistered(string name, address indexed token); - event TokenUnregistered(string name, address indexed token); - } - } -} - -pub use bank_inner::Bank; - -/// EIP-2535 diamond cut interface (`transfer/diamond/interfaces/IDiamondCut.sol`). -#[allow(clippy::too_many_arguments, unused_attributes)] -mod diamond_cut_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IDiamondCut { - /// `action` is FacetCutAction as its ABI underlying type (0=Add, - /// 1=Replace, 2=Remove); enums encode as uint8, so this yields the - /// same `diamondCut` selector as the Solidity interface. - struct FacetCut { - address facetAddress; - uint8 action; - bytes4[] functionSelectors; - } - function diamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata) external; - - event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata); - } - } -} - -pub use diamond_cut_inner::IDiamondCut; - -/// EIP-2535 loupe + ERC-173 ownership -/// (`transfer/diamond/interfaces/IDiamondLoupe.sol`, `IERC173.sol`). -#[allow(clippy::too_many_arguments, unused_attributes)] -mod diamond_loupe_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IDiamondLoupe { - struct Facet { - address facetAddress; - bytes4[] functionSelectors; - } - function facets() external view returns (Facet[] memory facets_); - function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory); - function facetAddresses() external view returns (address[] memory); - function facetAddress(bytes4 _functionSelector) external view returns (address); - function supportsInterface(bytes4 _interfaceId) external view returns (bool); - } - } - - sol! { - #[sol(rpc)] - interface IERC173 { - function owner() external view returns (address owner_); - function transferOwnership(address _newOwner) external; - } - } -} - -pub use diamond_loupe_inner::{ - IDiamondLoupe, - IERC173, -}; - -/// One-shot diamond initializer (`transfer/bank/BankInit.sol`). No -/// `#[sol(rpc)]`: only `initCall` is used — ABI-encoded as the diamondCut -/// `_init` delegatecall calldata that runs BankInit in the diamond's storage. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod bank_init_inner { - use alloy::sol; - - sol! { - interface BankInit { - function init(address notary, address backend, address registry) external; - } - } -} - -pub use bank_init_inner::BankInit; diff --git a/rust/contracts/src/deploy.rs b/rust/contracts/src/deploy.rs index dceb2e39..5e05a263 100644 --- a/rust/contracts/src/deploy.rs +++ b/rust/contracts/src/deploy.rs @@ -226,10 +226,13 @@ pub async fn upgrade_uups( } /// Load a contract's creation bytecode, deploying and linking any external -/// libraries it references (the bb-generated UltraHonk verifiers link -/// `ZKTranscriptLib`). Mirrors what `forge` does automatically. For artifacts -/// with no `linkReferences` this behaves like [`Artifacts::bytecode_named`] -/// and sends nothing. +/// libraries it references. Mirrors what `forge` does automatically. For +/// artifacts with no `linkReferences` this behaves like +/// [`Artifacts::bytecode_named`] and sends nothing. +/// +/// Kept although nothing covered today links a library: the bb-generated +/// UltraHonk verifiers link `ZKTranscriptLib`, and the ones the ceremony +/// circuits release will deploy through this path when they land. pub async fn load_linked_bytecode( provider: &P, artifacts: &Artifacts, diff --git a/rust/contracts/src/diamond.rs b/rust/contracts/src/diamond.rs deleted file mode 100644 index 4f624f6b..00000000 --- a/rust/contracts/src/diamond.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! The Bank EIP-2535 diamond: deploy and facet-replace flows. Mirrors -//! `solidity/contracts/transfer/script/BankDiamondDeployer.sol`. - -use alloy::{ - primitives::{ - Address, - Bytes, - }, - providers::Provider, - sol_types::SolCall, -}; - -use crate::{ - artifacts::Artifacts, - bindings::transfer::{ - BankInit, - IDiamondCut, - }, - deploy::{ - deploy_contract_from, - deploy_with_ctor, - }, - error::{ - Error, - Result, - }, -}; - -/// Every facet cut into the Bank diamond after construction. -/// -/// `DiamondCutFacet` is absent on purpose: the Diamond constructor wires that -/// one in itself. Deploy and upgrade both read this list, so a facet added -/// for one is not forgotten by the other. -pub const BANK_FACETS: &[&str] = &[ - "DiamondLoupeFacet", - "OwnershipFacet", - "AdminFacet", - "VaultFacet", - "TransferFacet", -]; - -/// `IDiamondCut.FacetCutAction` values, as their ABI underlying `uint8`. -pub mod facet_cut_action { - /// Add new selectors. - pub const ADD: u8 = 0; - /// Replace the facet behind existing selectors. - pub const REPLACE: u8 = 1; - /// Remove selectors. - pub const REMOVE: u8 = 2; -} - -/// Deploy a fresh Bank as an EIP-2535 diamond and return the diamond address. -/// -/// Deploys DiamondCutFacet, constructs `Diamond(owner, cutFacet)` (the ctor -/// auto-registers the diamondCut selector, so the cut below must NOT re-add -/// DiamondCutFacet), deploys the five remaining facets + `BankInit`, then one -/// `diamondCut` that adds every facet's selectors and delegatecalls -/// `BankInit.init(notary, backend, registry)` to seed the trusted-party -/// pointers, ERC-165 ids, templates, and prefixes. -/// -/// `notary` is the shared Notary contract's proxy address; `backend` and -/// `registry` come from the login deploy. All three must be nonzero or the -/// init reverts. -pub async fn deploy_bank_diamond( - provider: &P, - artifacts: &Artifacts, - owner: Address, - notary_contract: Address, - backend: Address, - registry: Address, -) -> Result
{ - // DiamondCutFacet — the one facet the Diamond ctor wires in itself. - let cut_facet_addr = deploy_contract_from( - provider, - artifacts.bytecode("DiamondCutFacet")?, - "DiamondCutFacet", - None, - ) - .await?; - - // Diamond(address owner, address diamondCutFacet). - let ctor_args = - alloy::sol_types::SolValue::abi_encode_params(&(owner, cut_facet_addr)); - let diamond_addr = deploy_with_ctor( - provider, - &artifacts.bytecode("Diamond")?, - &ctor_args, - "Diamond", - None, - ) - .await?; - - // Remaining facets: deploy each, then read its selectors from the - // artifact. (DiamondCutFacet is intentionally absent — already cut in.) - let cut = deploy_facet_cut(provider, artifacts, facet_cut_action::ADD, None).await?; - - // One-shot initializer, delegatecalled by diamondCut in diamond storage. - let bank_init_addr = - deploy_contract_from(provider, artifacts.bytecode("BankInit")?, "BankInit", None) - .await?; - - let init_calldata: Bytes = BankInit::initCall { - notary: notary_contract, - backend, - registry, - } - .abi_encode() - .into(); - - let diamond = IDiamondCut::new(diamond_addr, provider); - diamond - .diamondCut(cut, bank_init_addr, init_calldata) - .send() - .await - .map_err(|e| Error::Rpc { - detail: format!("Diamond.diamondCut send failed: {e}"), - })? - .get_receipt() - .await - .map_err(|e| Error::Rpc { - detail: format!("Diamond.diamondCut confirmation failed: {e}"), - })?; - - Ok(diamond_addr) -} - -/// Upgrade a deployed Bank diamond: re-deploy every facet in [`BANK_FACETS`] -/// and REPLACE its selectors in one `diamondCut`. There is no implementation -/// slot to re-point — the diamond IS the storage, the facets are the code. -pub async fn replace_bank_facets( - provider: &P, - artifacts: &Artifacts, - diamond: Address, - sender: Option
, -) -> Result<()> { - let cut = - deploy_facet_cut(provider, artifacts, facet_cut_action::REPLACE, sender).await?; - let diamond = IDiamondCut::new(diamond, provider); - let call = diamond.diamondCut(cut, Address::ZERO, Bytes::new()); - let pending = - match sender { - Some(addr) => { - let nonce = provider.get_transaction_count(addr).await.map_err(|e| { - Error::Rpc { - detail: format!("Bank.diamondCut failed to fetch nonce: {e}"), - } - })?; - call.nonce(nonce).send().await - } - None => call.send().await, - } - .map_err(|e| Error::Rpc { - detail: format!("Bank.diamondCut send failed: {e}"), - })?; - pending.get_receipt().await.map_err(|e| Error::Rpc { - detail: format!("Bank.diamondCut confirmation failed: {e}"), - })?; - Ok(()) -} - -/// Deploy every facet in [`BANK_FACETS`] and build the corresponding -/// `FacetCut` array with `action` for each facet's full selector set. -async fn deploy_facet_cut( - provider: &P, - artifacts: &Artifacts, - action: u8, - sender: Option
, -) -> Result> { - let mut cut = Vec::with_capacity(BANK_FACETS.len()); - for &name in BANK_FACETS { - let facet_addr = - deploy_contract_from(provider, artifacts.bytecode(name)?, name, sender) - .await?; - cut.push(IDiamondCut::FacetCut { - facetAddress: facet_addr, - action, - functionSelectors: artifacts.facet_selectors(name)?, - }); - } - Ok(cut) -} diff --git a/rust/contracts/src/lib.rs b/rust/contracts/src/lib.rs index bd160408..597439f4 100644 --- a/rust/contracts/src/lib.rs +++ b/rust/contracts/src/lib.rs @@ -1,21 +1,22 @@ //! Typed bindings, embedded forge artifacts, and deploy/upgrade helpers for -//! the libid contract stack. +//! the libid identity stack. //! -//! The crate has three layers: +//! The crate has four layers: //! //! - [`bindings`] — hand-written `alloy::sol!` interfaces for every contract a -//! consumer talks to: the login stack (`Registry`, `WebWallet`, -//! `WalletFactory`, verifiers), the Bank diamond, and the identity-names -//! stack. Kept in lockstep with the Solidity sources in `solidity/contracts`. +//! consumer talks to: the ceremony verification path (`NotaryService`, +//! `CeremonyProofVerifier`), the naming system (`IdentityNames`) with its +//! Google JWKS trust list (`IdentityJwksRoots`), 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 //! directory-backed variant ([`Artifacts::from_dir`]) reads a forge `out/` //! tree instead. -//! - [`deploy`] / [`diamond`] — generic deploy and upgrade primitives over any -//! alloy [`Provider`](alloy::providers::Provider): plain deploys, constructor -//! args, ERC1967 proxies, library linking, UUPS upgrades, and the Bank -//! EIP-2535 diamond deploy/facet-replace flows. +//! - [`deploy`] — generic deploy and upgrade primitives over any alloy +//! [`Provider`](alloy::providers::Provider): plain deploys, constructor +//! args, ERC1967 proxies, library linking, and UUPS upgrades. //! - [`factory`] — the deterministic-factory bootstrap: predict the canonical //! cross-network factory address, install it (and the keyless CREATE2 //! deployer it hangs off) where missing, and deploy protocol proxies @@ -27,7 +28,6 @@ pub mod artifacts; pub mod bindings; pub mod deploy; -pub mod diamond; mod error; pub mod factory; diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index 82ff6f14..dbac798b 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -1,5 +1,6 @@ -//! Anvil integration tests: deploy each stack from the embedded artifacts and -//! read a view back. Requires the `anvil` binary on PATH (foundry). +//! Anvil integration tests: deploy the identity stack from the embedded +//! artifacts and read views back. Requires the `anvil` binary on PATH +//! (foundry). use alloy::{ primitives::{ @@ -14,28 +15,20 @@ use alloy::{ }; use libid_contracts::{ bindings::{ - identity::IdentityNames, - login::{ - IRegistryAdmin, - Registry, - WalletFactory, + ceremony::{ + CeremonyProofVerifier, + NotaryService, }, - notary::Notary, - transfer::{ - Bank, - IDiamondLoupe, + identity::{ + IdentityJwksRoots, + IdentityNames, }, }, deploy::{ deploy_behind_proxy, deploy_contract, - load_linked_bytecode, upgrade_uups, }, - diamond::{ - deploy_bank_diamond, - BANK_FACETS, - }, Artifacts, }; @@ -47,170 +40,79 @@ async fn default_signer(provider: &impl Provider) -> Address { provider.get_accounts().await.expect("accounts")[0] } -/// (a) The login stack: Notary behind ERC1967 proxy first, then WebWallet -/// impl + WalletFactory behind proxy + Registry behind proxy pointing at the -/// Notary, wired together, then view calls. +/// (a) The identity stack in the order `script/Deploy.s.sol` uses: the +/// Notary Service first, then the Proof Verifier, the naming system given a +/// keyspace and pointed at the Proof Verifier, and the JWKS root list pointed +/// at the Notary Service — every one behind an ERC1967 proxy. Then the views +/// that prove the wiring took. #[tokio::test] -async fn deploys_the_login_stack_behind_proxies() { +async fn deploys_the_identity_stack_behind_proxies() { let provider = test_provider(); let artifacts = Artifacts::embedded(); let deployer = default_signer(&provider).await; - let notary = Address::repeat_byte(0x11); - let backend = Address::repeat_byte(0x22); + let notary_key = Address::repeat_byte(0x11); + let fee = U256::from(1_000); let notary_proxy = deploy_behind_proxy( &provider, &artifacts, - "Notary", - &Notary::initializeCall { + "NotaryService", + &NotaryService::initializeCall { owner_: deployer, - notary_: notary, + notary_: notary_key, + fee_: fee, }, None, ) .await .unwrap(); - let wallet_impl = deploy_contract( + let verifier_proxy = deploy_behind_proxy( &provider, - artifacts.bytecode("WebWallet").unwrap(), - "WebWallet (impl)", + &artifacts, + "CeremonyProofVerifier", + &CeremonyProofVerifier::initializeCall { owner_: deployer }, + None, ) .await .unwrap(); - let factory_proxy = deploy_behind_proxy( + let names_proxy = deploy_behind_proxy( &provider, &artifacts, - "WalletFactory", - &WalletFactory::initializeCall { - owner_: deployer, - walletImpl_: wallet_impl, - registry_: Address::ZERO, // registry not yet deployed - }, + "IdentityNames", + &IdentityNames::initializeCall { owner_: deployer }, None, ) .await .unwrap(); - let registry_proxy = deploy_behind_proxy( + let roots_proxy = deploy_behind_proxy( &provider, &artifacts, - "Registry", - &IRegistryAdmin::initializeCall { - _notaryContract: notary_proxy, - _backend: backend, - _walletFactory: factory_proxy, - _owner: deployer, + "IdentityJwksRoots", + &IdentityJwksRoots::initializeCall { + owner_: deployer, + notary_: notary_proxy, }, None, ) .await .unwrap(); - let factory = WalletFactory::new(factory_proxy, &provider); - factory - .setRegistry(registry_proxy) + // Wire the naming system: the Proof Verifier it dispatches through, and + // a keyspace. The platform id is the platform's own bare name: libID + // namespaces only its own strings. + let names = IdentityNames::new(names_proxy, &provider); + names + .setProofVerifier(verifier_proxy) .send() .await .unwrap() .get_receipt() .await .unwrap(); - - let registry = Registry::new(registry_proxy, &provider); - // notaryContract() is the pointer; notary() reads THROUGH it. - assert_eq!( - registry.notaryContract().call().await.unwrap(), - notary_proxy - ); - assert_eq!(registry.notary().call().await.unwrap(), notary); - assert_eq!(registry.backend().call().await.unwrap(), backend); - assert_eq!( - registry.walletFactory().call().await.unwrap(), - factory_proxy - ); - let factory = WalletFactory::new(factory_proxy, &provider); - assert_eq!( - factory.latestImplementation().call().await.unwrap(), - wallet_impl - ); -} - -/// (b) The full Bank diamond via the diamond helper + BankInit, then loupe -/// and Bank views. -#[tokio::test] -async fn deploys_the_bank_diamond() { - let provider = test_provider(); - let artifacts = Artifacts::embedded(); - let deployer = default_signer(&provider).await; - - let bank_addr = deploy_bank_diamond( - &provider, - &artifacts, - deployer, - Address::repeat_byte(0x11), // notary registry - Address::repeat_byte(0x22), // backend - Address::repeat_byte(0x33), // registry - ) - .await - .unwrap(); - - // Loupe: DiamondCutFacet (ctor-wired) + the five BANK_FACETS. - let loupe = IDiamondLoupe::new(bank_addr, &provider); - let facets = loupe.facetAddresses().call().await.unwrap(); - assert_eq!(facets.len(), BANK_FACETS.len() + 1); - - // BankInit ran in diamond storage: the registry pointer is seeded and the - // initialize templates exist for both platforms. - let bank = Bank::new(bank_addr, &provider); - assert_eq!( - bank.registry().call().await.unwrap(), - Address::repeat_byte(0x33) - ); - assert!(!bank.paused().call().await.unwrap()); - // BankInit seeds 4 templates for api.x.com (2 recipient-full + 2 - // recipient-less) and 2 for api.github.com. - assert_eq!( - bank.platformTemplateCount("api.x.com".into()) - .call() - .await - .unwrap(), - U256::from(4) - ); - assert_eq!( - bank.platformTemplateCount("api.github.com".into()) - .call() - .await - .unwrap(), - U256::from(2) - ); -} - -/// (c) IdentityNames behind proxy, given a keyspace with setPlatform, then -/// views. Wiring a verifier belongs to CeremonyProofVerifier now. -#[tokio::test] -async fn deploys_the_identity_stack() { - let provider = test_provider(); - let artifacts = Artifacts::embedded(); - let deployer = default_signer(&provider).await; - - let names_proxy = deploy_behind_proxy( - &provider, - &artifacts, - "IdentityNames", - &IdentityNames::initializeCall { owner_: deployer }, - None, - ) - .await - .unwrap(); - - // The platform id is the platform's own bare name: libID namespaces - // only its own strings. let platform_id = keccak256(b"github"); - let names = IdentityNames::new(names_proxy, &provider); - // The keyspace and the proof format are configured separately: rules do - // not vary by version, verifiers do. names .setPlatform( platform_id, @@ -229,127 +131,123 @@ async fn deploys_the_identity_stack() { .await .unwrap(); - // Wiring a verifier is the Proof Verifier's call now, not this contract's, - // and this deploy stops at the keyspace. A platform that owns a keyspace - // and can verify nothing says so: answering `address(0)` would tell the - // caller "nobody holds this name" about a platform that is not wired yet. + // The Notary Service holds the key and the fee it was given. + let notary = NotaryService::new(notary_proxy, &provider); + assert!(notary.isTrustedNotary(notary_key).call().await.unwrap()); + assert_eq!(notary.fee().call().await.unwrap(), fee); + + // Nothing is registered against any version yet, and the Proof Verifier + // says so rather than answering for a platform it cannot verify. + let verifier = CeremonyProofVerifier::new(verifier_proxy, &provider); + assert!(!verifier.verifiesPlatform(platform_id).call().await.unwrap()); + assert_eq!( + verifier.verifierOf(platform_id, 1).call().await.unwrap(), + Address::ZERO + ); + + assert_eq!(names.proofVerifier().call().await.unwrap(), verifier_proxy); + // A platform that owns a keyspace and can verify nothing says so: + // answering `address(0)` would tell the caller "nobody holds this name" + // about a platform that is not wired yet. let unwired = names.resolveId(platform_id, "12345".into()).call().await; assert!( unwired.is_err(), "an unwired platform answered instead of reverting UnknownPlatform" ); -} -/// (d) XHonkVerifier via linked bytecode — exercises the recursive library -/// deploy + link-reference substitution (ZKTranscriptLib). -#[tokio::test] -async fn deploys_the_x_honk_verifier_via_library_linking() { - // The generated UltraHonk verifiers exceed EIP-170 (the target chains - // raise the limit); anvil must be told to accept them. - let provider = ProviderBuilder::new() - .connect_anvil_with_wallet_and_config(|anvil| { - anvil.arg("--disable-code-size-limit") - }) - .expect("anvil spawns"); - let artifacts = Artifacts::embedded(); - - // The raw artifact must refuse the unlinked path… - assert!(artifacts.bytecode("XHonkVerifier").is_err()); - - // …and link + deploy through the linking path. - let linked = load_linked_bytecode( - &provider, - &artifacts, - "XHonkVerifier", - "XHonkVerifier", - None, - ) - .await - .unwrap(); - let addr = deploy_contract(&provider, linked, "XHonkVerifier") - .await - .unwrap(); - - let code = provider.get_code_at(addr).await.unwrap(); - assert!(!code.is_empty(), "XHonkVerifier has no runtime code"); - - // The Google OIDC circuit verifier links the same library under - // Verifier.sol/HonkVerifier — cover the file != contract path too. - let linked = artifacts - .linked_bytecode(&provider, "Verifier", "HonkVerifier", None) - .await - .unwrap(); - let addr = deploy_contract(&provider, linked, "HonkVerifier") - .await - .unwrap(); - assert!(!provider.get_code_at(addr).await.unwrap().is_empty()); + // The root list points at the Notary Service, quotes its fee, and starts + // empty — so it wants a rotation before any Google name can bind. + let roots = IdentityJwksRoots::new(roots_proxy, &provider); + assert_eq!(roots.notaryService().call().await.unwrap(), notary_proxy); + assert_eq!(roots.quoteRotation().call().await.unwrap(), fee); + assert!(roots.needsRotation().call().await.unwrap()); + assert!(roots.currentRoots().call().await.unwrap().is_empty()); } -/// (e) The Notary lifecycle: deploy behind a proxy, rotate the signer with -/// `setNotary`, upgrade the proxy to a freshly deployed implementation via -/// `upgrade_uups`, and check the rotated state survives the upgrade. +/// (b) The Notary Service lifecycle: deploy behind a proxy, add a second +/// trusted key with `setNotary`, change the fee with `setFee`, upgrade the +/// proxy to a freshly deployed implementation via `upgrade_uups`, and check +/// the rotated state survives the upgrade. #[tokio::test] -async fn rotates_and_upgrades_the_notary() { +async fn rotates_and_upgrades_the_notary_service() { let provider = test_provider(); let artifacts = Artifacts::embedded(); let deployer = default_signer(&provider).await; let first = Address::repeat_byte(0x11); - let rotated = Address::repeat_byte(0x33); + let incoming = Address::repeat_byte(0x33); let notary_proxy = deploy_behind_proxy( &provider, &artifacts, - "Notary", - &Notary::initializeCall { + "NotaryService", + &NotaryService::initializeCall { owner_: deployer, notary_: first, + fee_: U256::ZERO, }, None, ) .await .unwrap(); - let notary = Notary::new(notary_proxy, &provider); - assert_eq!(notary.notary().call().await.unwrap(), first); + let notary = NotaryService::new(notary_proxy, &provider); + assert!(notary.isTrustedNotary(first).call().await.unwrap()); + assert!(!notary.isTrustedNotary(incoming).call().await.unwrap()); + assert_eq!(notary.fee().call().await.unwrap(), U256::ZERO); - // A garbage proof answers false rather than reverting. - assert!(!notary - .verify(keccak256(b"digest"), vec![0u8; 65].into()) - .call() + // Rotate: the incoming key is trusted before the outgoing one goes, so + // attestations already made under `first` stay presentable meanwhile. + notary + .setNotary(incoming, true) + .send() .await - .unwrap()); - - // Rotate the signer. + .unwrap() + .get_receipt() + .await + .unwrap(); + notary + .setNotary(first, false) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); notary - .setNotary(rotated) + .setFee(U256::from(7)) .send() .await .unwrap() .get_receipt() .await .unwrap(); - assert_eq!(notary.notary().call().await.unwrap(), rotated); + assert!(!notary.isTrustedNotary(first).call().await.unwrap()); + assert!(notary.isTrustedNotary(incoming).call().await.unwrap()); + assert_eq!(notary.fee().call().await.unwrap(), U256::from(7)); // Upgrade the proxy to a re-deployed implementation; state survives. let new_impl = upgrade_uups( &provider, &artifacts, notary_proxy, - "Notary", + "NotaryService", Default::default(), None, ) .await .unwrap(); assert_ne!(new_impl, notary_proxy); - assert_eq!(notary.notary().call().await.unwrap(), rotated); + assert!(!notary.isTrustedNotary(first).call().await.unwrap()); + assert!(notary.isTrustedNotary(incoming).call().await.unwrap()); + assert_eq!(notary.fee().call().await.unwrap(), U256::from(7)); + assert_eq!(notary.owner().call().await.unwrap(), deployer); } -/// (f) The deterministic factory, from truly nothing: anvil is started +/// (c) The deterministic factory, from truly nothing: anvil is started /// WITHOUT its predeployed CREATE2 deployer, so `ensure_factory` must /// install it via the keyless presigned transaction (funding the one-time /// signer first), then deploy the factory impl + proxy at their canonical -/// addresses. Then a Notary proxy goes through the factory at its +/// addresses. Then a Notary Service proxy goes through the factory at its /// name-derived CREATE3 address. #[tokio::test] async fn bootstraps_the_deterministic_factory_and_deploys_through_it() { @@ -446,19 +344,21 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() { .unwrap(); assert_eq!(factory_contract.owner().call().await.unwrap(), deployer0); - // A Notary PROXY through the factory: impl via plain CREATE (its address - // doesn't matter), proxy creation code = ERC1967Proxy ++ (impl, initData). - let notary_signer = Address::repeat_byte(0x11); + // A Notary Service PROXY through the factory: impl via plain CREATE (its + // address doesn't matter), proxy creation code = ERC1967Proxy ++ (impl, + // initData). + let notary_key = Address::repeat_byte(0x11); let notary_impl = deploy_contract( &provider, - artifacts.bytecode("Notary").unwrap(), - "Notary (impl)", + artifacts.bytecode("NotaryService").unwrap(), + "NotaryService (impl)", ) .await .unwrap(); - let init_data = Notary::initializeCall { + let init_data = NotaryService::initializeCall { owner_: deployer0, - notary_: notary_signer, + notary_: notary_key, + fee_: U256::from(7), } .abi_encode(); let mut creation_code = artifacts.bytecode("ERC1967Proxy").unwrap().to_vec(); @@ -472,6 +372,8 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() { .unwrap(); assert_eq!(deployed, predicted); - let notary = Notary::new(deployed, &provider); - assert_eq!(notary.notary().call().await.unwrap(), notary_signer); + let notary = NotaryService::new(deployed, &provider); + assert!(notary.isTrustedNotary(notary_key).call().await.unwrap()); + assert_eq!(notary.fee().call().await.unwrap(), U256::from(7)); + assert_eq!(notary.owner().call().await.unwrap(), deployer0); } diff --git a/scripts/regen-verifiers.sh b/scripts/regen-verifiers.sh deleted file mode 100755 index a1790eff..00000000 --- a/scripts/regen-verifiers.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# Regenerate the bb-generated Solidity verifiers from the pinned -# libid-circuits release. -# -# solidity/circuits-version pins a release tag on libid-org/libid-circuits. -# This script downloads that release's artifacts (per-circuit tarballs plus a -# manifest with their sha256s and the toolchain versions that produced them), -# verifies the hashes, obtains bb at exactly the manifest's version, and -# re-derives both committed verifiers from the released verification keys: -# -# solidity/contracts/login/oidc/Verifier.sol ⟵ jwt_email vk -# solidity/contracts/login/zk/XHonkVerifier.sol ⟵ x-token vk -# (HonkVerifier renamed -# XHonkVerifier — two bb -# verifiers in one -# project would collide) -# -# Post-processing mirrors libid-circuits' scripts/gen-verifier.sh: every -# assembly block is annotated ("memory-safe") for via_ir, then the output is -# formatted under solidity/foundry.toml. -# -# Usage: -# scripts/regen-verifiers.sh --check # byte-compare against the committed -# # files; nonzero + diffstat on drift -# # (what CI's `verifiers` job runs) -# scripts/regen-verifiers.sh --write # overwrite the committed files -# -# Requirements: gh (authenticated; GH_TOKEN in CI), jq, curl, perl, forge. -# bb is used from PATH (or ~/.bb) when it already matches the manifest's -# version; otherwise bbup installs the pinned version into ~/.bb. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -MODE="${1:-}" -case "$MODE" in - --check|--write) ;; - *) - echo "usage: $0 --check|--write" >&2 - exit 2 - ;; -esac - -TAG="$(cat "$REPO_ROOT/solidity/circuits-version")" -echo "Pinned libid-circuits release: $TAG" - -# Download dir is a real temp dir; the generation dir lives under solidity/ -# (gitignored) because forge fmt formats it under solidity/foundry.toml. -DL="$(mktemp -d "${TMPDIR:-/tmp}/libid-circuits.XXXXXX")" -GEN="$REPO_ROOT/solidity/.circuits-check" -cleanup() { rm -rf "$DL" "$GEN"; } -trap cleanup EXIT -rm -rf "$GEN" -mkdir -p "$GEN" - -# --- Download the release assets ------------------------------------------- -if ! gh release download "$TAG" \ - --repo libid-org/libid-circuits \ - --dir "$DL" \ - --pattern 'libid-circuits-*.tar.gz' \ - --pattern 'manifest.json'; then - echo "::error::libid-circuits release '$TAG' (pinned in solidity/circuits-version) does not exist or has no artifacts yet. Cut that release first, or fix the pin." >&2 - exit 1 -fi - -# --- Verify tarball sha256s against the manifest --------------------------- -# Every tarball must hash to what the manifest promises before anything -# inside it is trusted. -sha256() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d' ' -f1 - else - shasum -a 256 "$1" | cut -d' ' -f1 # macOS - fi -} -for tarball in "$DL"/libid-circuits-*.tar.gz; do - name="$(basename "$tarball")" - want="$(jq -re --arg t "$name" '.tarballs[$t].sha256' "$DL/manifest.json")" - got="$(sha256 "$tarball")" - if [ "$want" != "$got" ]; then - echo "::error::$name sha256 mismatch: manifest says $want, asset is $got" >&2 - exit 1 - fi - echo "$name: sha256 OK" -done - -# --- Extract the circuits we regenerate from ------------------------------- -VERSION="$(jq -re .version "$DL/manifest.json")" -for circuit in jwt_email x-token; do - mkdir -p "$DL/$circuit" - tar -xzf "$DL/libid-circuits-${VERSION}-${circuit}.tar.gz" -C "$DL/$circuit" -done - -# --- bb at the manifest's toolchain version -------------------------------- -# The bb pin comes from the manifest — the release names the version that -# produced its vks, and only that version reproduces them. An existing bb at -# the right version (on PATH or in ~/.bb) is used as-is; otherwise bbup -# installs the pinned version into ~/.bb. -BB_VERSION="$(jq -re .toolchain.bb "$DL/manifest.json")" -BB="" -for candidate in "$(command -v bb || true)" "$HOME/.bb/bb"; do - if [ -n "$candidate" ] && [ -x "$candidate" ] \ - && [ "$("$candidate" --version | tail -1)" = "$BB_VERSION" ]; then - BB="$candidate" - break - fi -done -if [ -z "$BB" ]; then - echo "No bb $BB_VERSION found; installing via bbup." - if ! command -v bbup >/dev/null 2>&1 && [ ! -x "$HOME/.bb/bbup" ]; then - curl -L https://raw.githubusercontent.com/AztecProtocol/aztec-packages/master/barretenberg/bbup/install | bash - fi - PATH="$HOME/.bb:$PATH" bbup --version "$BB_VERSION" - BB="$HOME/.bb/bb" - [ "$("$BB" --version | tail -1)" = "$BB_VERSION" ] || { - echo "::error::bbup installed $("$BB" --version | tail -1), wanted $BB_VERSION" >&2 - exit 1 - } -fi -echo "Using bb $BB_VERSION at $BB" - -# --- Regenerate ------------------------------------------------------------ -"$BB" write_solidity_verifier -k "$DL/jwt_email/vk" \ - -o "$GEN/Verifier.sol" -t evm -"$BB" write_solidity_verifier -k "$DL/x-token/vk" \ - -o "$GEN/XHonkVerifier.sol" -t evm - -perl -i -pe 's/assembly \{/assembly ("memory-safe") \{/g' \ - "$GEN/Verifier.sol" "$GEN/XHonkVerifier.sol" -perl -i -pe 's/contract HonkVerifier is BaseZKHonkVerifier/contract XHonkVerifier is BaseZKHonkVerifier/g' \ - "$GEN/XHonkVerifier.sol" - -(cd "$REPO_ROOT/solidity" && forge fmt .circuits-check/Verifier.sol .circuits-check/XHonkVerifier.sol) - -# --- Compare or write ------------------------------------------------------ -PAIRS=( - "$GEN/Verifier.sol:$REPO_ROOT/solidity/contracts/login/oidc/Verifier.sol" - "$GEN/XHonkVerifier.sol:$REPO_ROOT/solidity/contracts/login/zk/XHonkVerifier.sol" -) - -status=0 -for pair in "${PAIRS[@]}"; do - fresh="${pair%%:*}" - committed="${pair##*:}" - rel="${committed#"$REPO_ROOT"/}" - if [ "$MODE" = "--write" ]; then - if diff -q "$fresh" "$committed" >/dev/null 2>&1; then - echo "$rel is already current." - else - cp "$fresh" "$committed" - echo "$rel rewritten from the $TAG release vk." - fi - elif diff -q "$fresh" "$committed" >/dev/null; then - echo "$rel reproduces from the release vk." - else - echo "::error::$rel does not match the verifier regenerated from the pinned circuits release" >&2 - git diff --no-index --stat "$committed" "$fresh" || true - git diff --no-index "$committed" "$fresh" | head -60 || true - status=1 - fi -done -exit "$status" diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index 5269eafa..5ce31e9d 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -5,8 +5,9 @@ # the artifact JSONs the crate needs from solidity/out into # rust/contracts/artifacts/.sol/.json, pruned to the fields the # crate reads: bytecode.object, bytecode.linkReferences, methodIdentifiers. -# Libraries referenced through linkReferences (e.g. ZKTranscriptLib for the -# Honk verifiers) are followed transitively and vendored too. +# Libraries referenced through linkReferences are followed transitively and +# vendored too (none of the covered contracts links one today; the Honk +# verifiers the ceremony circuits will bring do). # # The result is NOT committed: rust/contracts/artifacts is gitignored and # regenerated on demand. Run this before any cargo command in rust/ — the @@ -27,35 +28,16 @@ DEST="$REPO_ROOT/rust/contracts/artifacts" # ":" — the artifact lives at out/.sol/.json. # Keep in sync with the covered-contract list in rust/contracts/src/artifacts.rs. ARTIFACTS=( - # login - "Registry:Registry" - "Notary:Notary" - "WalletFactory:WalletFactory" - "WebWallet:WebWallet" + # ceremony + "NotaryService:NotaryService" + "CeremonyProofVerifier:CeremonyProofVerifier" "ERC1967Proxy:ERC1967Proxy" - "XZkVerifier:XZkVerifier" - "XHonkVerifier:XHonkVerifier" - # oidc (the Google OIDC circuit verifier is generated into Verifier.sol - # under the contract name HonkVerifier) - "Verifier:HonkVerifier" - "GoogleOidcVerifier:GoogleOidcVerifier" - # transfer (bank diamond) - "Diamond:Diamond" - "DiamondCutFacet:DiamondCutFacet" - "DiamondLoupeFacet:DiamondLoupeFacet" - "OwnershipFacet:OwnershipFacet" - "AdminFacet:AdminFacet" - "VaultFacet:VaultFacet" - "TransferFacet:TransferFacet" - "BankInit:BankInit" - "IBank:IBank" - "MockERC20:MockERC20" - "WTIA9:WTIA9" # identity "IdentityNames:IdentityNames" "IdentityJwksRoots:IdentityJwksRoots" # factory "LibidFactory:LibidFactory" + "WTIA9:WTIA9" ) if [[ $# -gt 0 ]]; then diff --git a/solidity/archived/README.md b/solidity/archived/README.md new file mode 100644 index 00000000..f4cb5d0f --- /dev/null +++ b/solidity/archived/README.md @@ -0,0 +1,45 @@ +# Archived contracts + +Reference only. Nothing under this directory is compiled, tested, shipped, +or deployed by this repository. + +## What is here + +- `login/` — the wallet product's login stack: `Registry` (the one place a + notarized or zero-knowledge session became a platform → wallet binding), + `WebWallet` and `WalletFactory` (the smart-account wallets it minted), the + `XZkVerifier` ZK session verifier and the `GoogleOidcVerifier` OIDC verifier, + together with the two bb-generated UltraHonk verifiers they called + (`oidc/Verifier.sol`, `zk/XHonkVerifier.sol`), their tests, fixtures and + deploy scripts. +- `transfer/` — the transfer product: the `Bank` EIP-2535 diamond (facets, + init, libraries, storage), its deployer, the `MockERC20` dev token, and its + tests. +- `notary/` — the legacy `Notary`: a single-key attestation verifier that + took a caller-computed digest. Everything in `contracts/` now verifies + notary attestations through `ceremony/NotaryService`, which derives the + digest from the attested bytes itself (REQ-COMMON-49) and charges the + Notary Fee. + +They are kept, rather than deleted, as a record of how a full product was +built on notarized sessions: the login stack is the worked example of turning +a session into an on-chain binding, and the bank is the worked example of a +consumer spending one. Git history has every version of them; this directory +holds the last. + +## Status + +Frozen as of the commit that moved them here. They last built and passed +their tests at tag `v0.6.0`, against that tag's `foundry.toml` and +submodules. Nothing regenerates them: the bb-generated verifiers are not +re-derived from a circuits release, no Rust bindings or TypeScript ABIs are +produced for them, and CI does not look at them. + +This directory sits outside forge's `src`, `test` and `script` roots, so +`forge build` never sees it. Moving a file back under `contracts/` is what +would make it compile again — and it would need to be brought up to the +current bases (ERC-7201 namespaced storage + UUPS + Ownable2Step) and to +`NotaryService` before it could be deployed alongside the rest. + +The wallet product is not deployed by this repository any more. +`script/Deploy.s.sol` deploys the identity stack only. diff --git a/solidity/contracts/login/IRegistry.sol b/solidity/archived/login/IRegistry.sol similarity index 100% rename from solidity/contracts/login/IRegistry.sol rename to solidity/archived/login/IRegistry.sol diff --git a/solidity/contracts/login/Registry.sol b/solidity/archived/login/Registry.sol similarity index 100% rename from solidity/contracts/login/Registry.sol rename to solidity/archived/login/Registry.sol diff --git a/solidity/contracts/login/SnapshotProxy.sol b/solidity/archived/login/SnapshotProxy.sol similarity index 100% rename from solidity/contracts/login/SnapshotProxy.sol rename to solidity/archived/login/SnapshotProxy.sol diff --git a/solidity/contracts/login/WalletFactory.sol b/solidity/archived/login/WalletFactory.sol similarity index 100% rename from solidity/contracts/login/WalletFactory.sol rename to solidity/archived/login/WalletFactory.sol diff --git a/solidity/contracts/login/WebWallet.sol b/solidity/archived/login/WebWallet.sol similarity index 100% rename from solidity/contracts/login/WebWallet.sol rename to solidity/archived/login/WebWallet.sol diff --git a/solidity/contracts/login/oidc/GoogleOidcVerifier.sol b/solidity/archived/login/oidc/GoogleOidcVerifier.sol similarity index 100% rename from solidity/contracts/login/oidc/GoogleOidcVerifier.sol rename to solidity/archived/login/oidc/GoogleOidcVerifier.sol diff --git a/solidity/contracts/login/oidc/IOidcVerifier.sol b/solidity/archived/login/oidc/IOidcVerifier.sol similarity index 100% rename from solidity/contracts/login/oidc/IOidcVerifier.sol rename to solidity/archived/login/oidc/IOidcVerifier.sol diff --git a/solidity/contracts/login/oidc/Verifier.sol b/solidity/archived/login/oidc/Verifier.sol similarity index 100% rename from solidity/contracts/login/oidc/Verifier.sol rename to solidity/archived/login/oidc/Verifier.sol diff --git a/solidity/contracts/login/oidc/script/DeployOidc.s.sol b/solidity/archived/login/oidc/script/DeployOidc.s.sol similarity index 100% rename from solidity/contracts/login/oidc/script/DeployOidc.s.sol rename to solidity/archived/login/oidc/script/DeployOidc.s.sol diff --git a/solidity/contracts/login/test/Registry.t.sol b/solidity/archived/login/test/Registry.t.sol similarity index 100% rename from solidity/contracts/login/test/Registry.t.sol rename to solidity/archived/login/test/Registry.t.sol diff --git a/solidity/contracts/login/test/TlsLinkGriefPoC.t.sol b/solidity/archived/login/test/TlsLinkGriefPoC.t.sol similarity index 100% rename from solidity/contracts/login/test/TlsLinkGriefPoC.t.sol rename to solidity/archived/login/test/TlsLinkGriefPoC.t.sol diff --git a/solidity/contracts/login/test/WalletFactory.t.sol b/solidity/archived/login/test/WalletFactory.t.sol similarity index 100% rename from solidity/contracts/login/test/WalletFactory.t.sol rename to solidity/archived/login/test/WalletFactory.t.sol diff --git a/solidity/contracts/login/test/WebWallet.t.sol b/solidity/archived/login/test/WebWallet.t.sol similarity index 100% rename from solidity/contracts/login/test/WebWallet.t.sol rename to solidity/archived/login/test/WebWallet.t.sol diff --git a/solidity/contracts/login/test/fixtures/oidc/jwks-proof.json b/solidity/archived/login/test/fixtures/oidc/jwks-proof.json similarity index 100% rename from solidity/contracts/login/test/fixtures/oidc/jwks-proof.json rename to solidity/archived/login/test/fixtures/oidc/jwks-proof.json diff --git a/solidity/contracts/login/test/fixtures/oidc/jwks-snapshot.json b/solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json similarity index 100% rename from solidity/contracts/login/test/fixtures/oidc/jwks-snapshot.json rename to solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json diff --git a/solidity/contracts/login/test/oidc/GoogleOidcVerifierFlow.t.sol b/solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol similarity index 100% rename from solidity/contracts/login/test/oidc/GoogleOidcVerifierFlow.t.sol rename to solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol diff --git a/solidity/contracts/login/test/oidc/README.md b/solidity/archived/login/test/oidc/README.md similarity index 100% rename from solidity/contracts/login/test/oidc/README.md rename to solidity/archived/login/test/oidc/README.md diff --git a/solidity/contracts/login/test/oidc/RegistryOidcLinkFlow.t.sol b/solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol similarity index 100% rename from solidity/contracts/login/test/oidc/RegistryOidcLinkFlow.t.sol rename to solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol diff --git a/solidity/contracts/login/zk/IZkSessionVerifier.sol b/solidity/archived/login/zk/IZkSessionVerifier.sol similarity index 100% rename from solidity/contracts/login/zk/IZkSessionVerifier.sol rename to solidity/archived/login/zk/IZkSessionVerifier.sol diff --git a/solidity/contracts/login/zk/XHonkVerifier.sol b/solidity/archived/login/zk/XHonkVerifier.sol similarity index 100% rename from solidity/contracts/login/zk/XHonkVerifier.sol rename to solidity/archived/login/zk/XHonkVerifier.sol diff --git a/solidity/contracts/login/zk/XZkVerifier.sol b/solidity/archived/login/zk/XZkVerifier.sol similarity index 100% rename from solidity/contracts/login/zk/XZkVerifier.sol rename to solidity/archived/login/zk/XZkVerifier.sol diff --git a/solidity/contracts/login/zk/test/XZkVerifierAllowlist.t.sol b/solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol similarity index 100% rename from solidity/contracts/login/zk/test/XZkVerifierAllowlist.t.sol rename to solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol diff --git a/solidity/contracts/login/zk/test/XZkVerifierBindings.t.sol b/solidity/archived/login/zk/test/XZkVerifierBindings.t.sol similarity index 100% rename from solidity/contracts/login/zk/test/XZkVerifierBindings.t.sol rename to solidity/archived/login/zk/test/XZkVerifierBindings.t.sol diff --git a/solidity/contracts/login/zk/test/XZkVerifierFlow.t.sol b/solidity/archived/login/zk/test/XZkVerifierFlow.t.sol similarity index 100% rename from solidity/contracts/login/zk/test/XZkVerifierFlow.t.sol rename to solidity/archived/login/zk/test/XZkVerifierFlow.t.sol diff --git a/solidity/contracts/notary/INotary.sol b/solidity/archived/notary/INotary.sol similarity index 100% rename from solidity/contracts/notary/INotary.sol rename to solidity/archived/notary/INotary.sol diff --git a/solidity/contracts/notary/Notary.sol b/solidity/archived/notary/Notary.sol similarity index 100% rename from solidity/contracts/notary/Notary.sol rename to solidity/archived/notary/Notary.sol diff --git a/solidity/contracts/notary/test/DeployNotary.sol b/solidity/archived/notary/test/DeployNotary.sol similarity index 100% rename from solidity/contracts/notary/test/DeployNotary.sol rename to solidity/archived/notary/test/DeployNotary.sol diff --git a/solidity/contracts/notary/test/Notary.t.sol b/solidity/archived/notary/test/Notary.t.sol similarity index 100% rename from solidity/contracts/notary/test/Notary.t.sol rename to solidity/archived/notary/test/Notary.t.sol diff --git a/solidity/contracts/transfer/MockERC20.sol b/solidity/archived/transfer/MockERC20.sol similarity index 100% rename from solidity/contracts/transfer/MockERC20.sol rename to solidity/archived/transfer/MockERC20.sol diff --git a/solidity/contracts/transfer/bank/BankErrors.sol b/solidity/archived/transfer/bank/BankErrors.sol similarity index 100% rename from solidity/contracts/transfer/bank/BankErrors.sol rename to solidity/archived/transfer/bank/BankErrors.sol diff --git a/solidity/contracts/transfer/bank/BankInit.sol b/solidity/archived/transfer/bank/BankInit.sol similarity index 100% rename from solidity/contracts/transfer/bank/BankInit.sol rename to solidity/archived/transfer/bank/BankInit.sol diff --git a/solidity/contracts/transfer/bank/BankModifiers.sol b/solidity/archived/transfer/bank/BankModifiers.sol similarity index 100% rename from solidity/contracts/transfer/bank/BankModifiers.sol rename to solidity/archived/transfer/bank/BankModifiers.sol diff --git a/solidity/contracts/transfer/bank/BankTypes.sol b/solidity/archived/transfer/bank/BankTypes.sol similarity index 100% rename from solidity/contracts/transfer/bank/BankTypes.sol rename to solidity/archived/transfer/bank/BankTypes.sol diff --git a/solidity/contracts/transfer/bank/IBank.sol b/solidity/archived/transfer/bank/IBank.sol similarity index 100% rename from solidity/contracts/transfer/bank/IBank.sol rename to solidity/archived/transfer/bank/IBank.sol diff --git a/solidity/contracts/transfer/bank/facets/AdminFacet.sol b/solidity/archived/transfer/bank/facets/AdminFacet.sol similarity index 100% rename from solidity/contracts/transfer/bank/facets/AdminFacet.sol rename to solidity/archived/transfer/bank/facets/AdminFacet.sol diff --git a/solidity/contracts/transfer/bank/facets/TransferFacet.sol b/solidity/archived/transfer/bank/facets/TransferFacet.sol similarity index 100% rename from solidity/contracts/transfer/bank/facets/TransferFacet.sol rename to solidity/archived/transfer/bank/facets/TransferFacet.sol diff --git a/solidity/contracts/transfer/bank/facets/VaultFacet.sol b/solidity/archived/transfer/bank/facets/VaultFacet.sol similarity index 100% rename from solidity/contracts/transfer/bank/facets/VaultFacet.sol rename to solidity/archived/transfer/bank/facets/VaultFacet.sol diff --git a/solidity/contracts/transfer/bank/libraries/LibEscrow.sol b/solidity/archived/transfer/bank/libraries/LibEscrow.sol similarity index 100% rename from solidity/contracts/transfer/bank/libraries/LibEscrow.sol rename to solidity/archived/transfer/bank/libraries/LibEscrow.sol diff --git a/solidity/contracts/transfer/bank/libraries/LibProof.sol b/solidity/archived/transfer/bank/libraries/LibProof.sol similarity index 100% rename from solidity/contracts/transfer/bank/libraries/LibProof.sol rename to solidity/archived/transfer/bank/libraries/LibProof.sol diff --git a/solidity/contracts/transfer/bank/libraries/LibRecipient.sol b/solidity/archived/transfer/bank/libraries/LibRecipient.sol similarity index 100% rename from solidity/contracts/transfer/bank/libraries/LibRecipient.sol rename to solidity/archived/transfer/bank/libraries/LibRecipient.sol diff --git a/solidity/contracts/transfer/bank/libraries/LibString.sol b/solidity/archived/transfer/bank/libraries/LibString.sol similarity index 100% rename from solidity/contracts/transfer/bank/libraries/LibString.sol rename to solidity/archived/transfer/bank/libraries/LibString.sol diff --git a/solidity/contracts/transfer/bank/libraries/LibTemplate.sol b/solidity/archived/transfer/bank/libraries/LibTemplate.sol similarity index 100% rename from solidity/contracts/transfer/bank/libraries/LibTemplate.sol rename to solidity/archived/transfer/bank/libraries/LibTemplate.sol diff --git a/solidity/contracts/transfer/bank/storage/LibCoreStorage.sol b/solidity/archived/transfer/bank/storage/LibCoreStorage.sol similarity index 100% rename from solidity/contracts/transfer/bank/storage/LibCoreStorage.sol rename to solidity/archived/transfer/bank/storage/LibCoreStorage.sol diff --git a/solidity/contracts/transfer/bank/storage/LibEscrowStorage.sol b/solidity/archived/transfer/bank/storage/LibEscrowStorage.sol similarity index 100% rename from solidity/contracts/transfer/bank/storage/LibEscrowStorage.sol rename to solidity/archived/transfer/bank/storage/LibEscrowStorage.sol diff --git a/solidity/contracts/transfer/bank/storage/LibTemplateStorage.sol b/solidity/archived/transfer/bank/storage/LibTemplateStorage.sol similarity index 100% rename from solidity/contracts/transfer/bank/storage/LibTemplateStorage.sol rename to solidity/archived/transfer/bank/storage/LibTemplateStorage.sol diff --git a/solidity/contracts/transfer/bank/storage/LibTokenStorage.sol b/solidity/archived/transfer/bank/storage/LibTokenStorage.sol similarity index 100% rename from solidity/contracts/transfer/bank/storage/LibTokenStorage.sol rename to solidity/archived/transfer/bank/storage/LibTokenStorage.sol diff --git a/solidity/contracts/transfer/diamond/Diamond.sol b/solidity/archived/transfer/diamond/Diamond.sol similarity index 100% rename from solidity/contracts/transfer/diamond/Diamond.sol rename to solidity/archived/transfer/diamond/Diamond.sol diff --git a/solidity/contracts/transfer/diamond/facets/DiamondCutFacet.sol b/solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol similarity index 100% rename from solidity/contracts/transfer/diamond/facets/DiamondCutFacet.sol rename to solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol diff --git a/solidity/contracts/transfer/diamond/facets/DiamondLoupeFacet.sol b/solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol similarity index 100% rename from solidity/contracts/transfer/diamond/facets/DiamondLoupeFacet.sol rename to solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol diff --git a/solidity/contracts/transfer/diamond/facets/OwnershipFacet.sol b/solidity/archived/transfer/diamond/facets/OwnershipFacet.sol similarity index 100% rename from solidity/contracts/transfer/diamond/facets/OwnershipFacet.sol rename to solidity/archived/transfer/diamond/facets/OwnershipFacet.sol diff --git a/solidity/contracts/transfer/diamond/interfaces/IDiamondCut.sol b/solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol similarity index 100% rename from solidity/contracts/transfer/diamond/interfaces/IDiamondCut.sol rename to solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol diff --git a/solidity/contracts/transfer/diamond/interfaces/IDiamondLoupe.sol b/solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol similarity index 100% rename from solidity/contracts/transfer/diamond/interfaces/IDiamondLoupe.sol rename to solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol diff --git a/solidity/contracts/transfer/diamond/interfaces/IERC173.sol b/solidity/archived/transfer/diamond/interfaces/IERC173.sol similarity index 100% rename from solidity/contracts/transfer/diamond/interfaces/IERC173.sol rename to solidity/archived/transfer/diamond/interfaces/IERC173.sol diff --git a/solidity/contracts/transfer/diamond/libraries/LibDiamond.sol b/solidity/archived/transfer/diamond/libraries/LibDiamond.sol similarity index 100% rename from solidity/contracts/transfer/diamond/libraries/LibDiamond.sol rename to solidity/archived/transfer/diamond/libraries/LibDiamond.sol diff --git a/solidity/contracts/transfer/script/BankDiamondDeployer.sol b/solidity/archived/transfer/script/BankDiamondDeployer.sol similarity index 100% rename from solidity/contracts/transfer/script/BankDiamondDeployer.sol rename to solidity/archived/transfer/script/BankDiamondDeployer.sol diff --git a/solidity/contracts/transfer/script/DeployBankDiamond.s.sol b/solidity/archived/transfer/script/DeployBankDiamond.s.sol similarity index 100% rename from solidity/contracts/transfer/script/DeployBankDiamond.s.sol rename to solidity/archived/transfer/script/DeployBankDiamond.s.sol diff --git a/solidity/contracts/transfer/test/BalanceViews.t.sol b/solidity/archived/transfer/test/BalanceViews.t.sol similarity index 100% rename from solidity/contracts/transfer/test/BalanceViews.t.sol rename to solidity/archived/transfer/test/BalanceViews.t.sol diff --git a/solidity/contracts/transfer/test/BankDiamond.t.sol b/solidity/archived/transfer/test/BankDiamond.t.sol similarity index 100% rename from solidity/contracts/transfer/test/BankDiamond.t.sol rename to solidity/archived/transfer/test/BankDiamond.t.sol diff --git a/solidity/contracts/transfer/test/BankDiamondHonor.t.sol b/solidity/archived/transfer/test/BankDiamondHonor.t.sol similarity index 100% rename from solidity/contracts/transfer/test/BankDiamondHonor.t.sol rename to solidity/archived/transfer/test/BankDiamondHonor.t.sol diff --git a/solidity/contracts/transfer/test/DollarToken.t.sol b/solidity/archived/transfer/test/DollarToken.t.sol similarity index 100% rename from solidity/contracts/transfer/test/DollarToken.t.sol rename to solidity/archived/transfer/test/DollarToken.t.sol diff --git a/solidity/contracts/transfer/test/EdgeCases.t.sol b/solidity/archived/transfer/test/EdgeCases.t.sol similarity index 100% rename from solidity/contracts/transfer/test/EdgeCases.t.sol rename to solidity/archived/transfer/test/EdgeCases.t.sol diff --git a/solidity/contracts/transfer/test/EscrowById.t.sol b/solidity/archived/transfer/test/EscrowById.t.sol similarity index 100% rename from solidity/contracts/transfer/test/EscrowById.t.sol rename to solidity/archived/transfer/test/EscrowById.t.sol diff --git a/solidity/contracts/transfer/test/ImplicitTemplate.t.sol b/solidity/archived/transfer/test/ImplicitTemplate.t.sol similarity index 100% rename from solidity/contracts/transfer/test/ImplicitTemplate.t.sol rename to solidity/archived/transfer/test/ImplicitTemplate.t.sol diff --git a/solidity/contracts/transfer/test/Integration.t.sol b/solidity/archived/transfer/test/Integration.t.sol similarity index 100% rename from solidity/contracts/transfer/test/Integration.t.sol rename to solidity/archived/transfer/test/Integration.t.sol diff --git a/solidity/contracts/transfer/test/IntegrationNewFeatures.t.sol b/solidity/archived/transfer/test/IntegrationNewFeatures.t.sol similarity index 100% rename from solidity/contracts/transfer/test/IntegrationNewFeatures.t.sol rename to solidity/archived/transfer/test/IntegrationNewFeatures.t.sol diff --git a/solidity/contracts/transfer/test/LibStringGuards.t.sol b/solidity/archived/transfer/test/LibStringGuards.t.sol similarity index 100% rename from solidity/contracts/transfer/test/LibStringGuards.t.sol rename to solidity/archived/transfer/test/LibStringGuards.t.sol diff --git a/solidity/contracts/transfer/test/NativeAsset.t.sol b/solidity/archived/transfer/test/NativeAsset.t.sol similarity index 100% rename from solidity/contracts/transfer/test/NativeAsset.t.sol rename to solidity/archived/transfer/test/NativeAsset.t.sol diff --git a/solidity/contracts/transfer/test/PausableFlow.t.sol b/solidity/archived/transfer/test/PausableFlow.t.sol similarity index 100% rename from solidity/contracts/transfer/test/PausableFlow.t.sol rename to solidity/archived/transfer/test/PausableFlow.t.sol diff --git a/solidity/contracts/transfer/test/RegistryBankRecycle.t.sol b/solidity/archived/transfer/test/RegistryBankRecycle.t.sol similarity index 100% rename from solidity/contracts/transfer/test/RegistryBankRecycle.t.sol rename to solidity/archived/transfer/test/RegistryBankRecycle.t.sol diff --git a/solidity/contracts/transfer/test/TemplateMatching.t.sol b/solidity/archived/transfer/test/TemplateMatching.t.sol similarity index 100% rename from solidity/contracts/transfer/test/TemplateMatching.t.sol rename to solidity/archived/transfer/test/TemplateMatching.t.sol diff --git a/solidity/contracts/transfer/test/TransferWithin.t.sol b/solidity/archived/transfer/test/TransferWithin.t.sol similarity index 100% rename from solidity/contracts/transfer/test/TransferWithin.t.sol rename to solidity/archived/transfer/test/TransferWithin.t.sol diff --git a/solidity/circuits-version b/solidity/circuits-version deleted file mode 100644 index 1474d00f..00000000 --- a/solidity/circuits-version +++ /dev/null @@ -1 +0,0 @@ -v0.2.0 diff --git a/solidity/contracts/WTIA9.sol b/solidity/contracts/WTIA9.sol index bb84f5ac..ec575d8f 100644 --- a/solidity/contracts/WTIA9.sol +++ b/solidity/contracts/WTIA9.sol @@ -17,9 +17,9 @@ pragma solidity ^0.8.20; /// /// 1. `withdraw` uses `call` rather than `transfer`. `transfer` forwards a /// fixed 2300 gas stipend, which is enough for an EOA but reverts for a -/// contract recipient whose `receive` does anything at all — including -/// this repo's own WebWallet, which emits an event on native receipt. -/// WETH9 predates that being a known hazard. +/// contract recipient whose `receive` does anything at all — a smart +/// account that logs an event on native receipt, say. WETH9 predates +/// that being a known hazard. /// 2. Balances are decremented *before* the external call /// (checks-effects-interactions), so a malicious `receive` cannot /// re-enter and withdraw twice. The original is safe only by accident diff --git a/solidity/contracts/factory/test/LibidFactory.t.sol b/solidity/contracts/factory/test/LibidFactory.t.sol index 674200f8..d822f193 100644 --- a/solidity/contracts/factory/test/LibidFactory.t.sol +++ b/solidity/contracts/factory/test/LibidFactory.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Notary} from "../../notary/Notary.sol"; +import {NotaryService} from "../../ceremony/NotaryService.sol"; import {Create3} from "../Create3.sol"; import {LibidFactory} from "../LibidFactory.sol"; import {Ping, Pong} from "./Create3.t.sol"; @@ -117,12 +117,12 @@ contract LibidFactoryTest is Test { /// The intended use: deploy an ERC1967 PROXY through the factory /// (creationCode = proxy creation code ++ abi.encode(impl, initData)); /// the proxied contract works and lives at the name's address. - function test_deploy_notaryProxyThroughFactory() public { + function test_deploy_notaryServiceProxyThroughFactory() public { address signer = makeAddr("notarySigner"); - Notary notaryImpl = new Notary(); + NotaryService notaryImpl = new NotaryService(); bytes memory creationCode = abi.encodePacked( type(ERC1967Proxy).creationCode, - abi.encode(address(notaryImpl), abi.encodeCall(Notary.initialize, (owner, signer))) + abi.encode(address(notaryImpl), abi.encodeCall(NotaryService.initialize, (owner, signer, 7))) ); address predicted = factory.predict("libid.notary"); @@ -130,15 +130,17 @@ contract LibidFactoryTest is Test { address deployed = factory.deploy("libid.notary", creationCode); assertEq(deployed, predicted); - Notary notary = Notary(deployed); + NotaryService notary = NotaryService(deployed); assertEq(notary.owner(), owner); - assertEq(notary.notary(), signer); + assertTrue(notary.isTrustedNotary(signer)); + assertEq(notary.fee(), 7); // …and it upgrades in place like any other proxy in the stack. - Notary newImpl = new Notary(); + NotaryService newImpl = new NotaryService(); vm.prank(owner); notary.upgradeToAndCall(address(newImpl), ""); - assertEq(notary.notary(), signer); + assertTrue(notary.isTrustedNotary(signer)); + assertEq(notary.fee(), 7); } // ─── UUPS upgrade of the factory itself ───────────────────────── diff --git a/solidity/contracts/identity/IdentityNames.sol b/solidity/contracts/identity/IdentityNames.sol index 9df22c0f..616c956d 100644 --- a/solidity/contracts/identity/IdentityNames.sol +++ b/solidity/contracts/identity/IdentityNames.sol @@ -198,8 +198,8 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl bytes32 private constant IDENTITY_NAMES_STORAGE = 0x064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf200; - /// @dev All state of this contract sits under one namespaced root, the way - /// `Registry` does. Three things follow. + /// @dev All state of this contract sits under one namespaced root, like + /// every upgradeable contract in this repo. Three things follow. /// /// It cannot collide with the ERC-7201 namespaces of the OpenZeppelin /// upgradeable bases, so this contract owns its slots outright and diff --git a/solidity/foundry.toml b/solidity/foundry.toml index cbd92504..1743d5b6 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -2,9 +2,10 @@ src = "contracts" out = "out" libs = ["lib"] -# Tests live beside the code they cover — contracts/login/test, contracts/ -# transfer/test, contracts/identity/test — so discovery goes through src -# rather than one test directory. +# Tests live beside the code they cover — contracts/ceremony/test, contracts/ +# identity/test, contracts/factory/test — so discovery goes through src +# rather than one test directory. archived/ is outside every root on +# purpose: what it holds is reference, not source (see archived/README.md). test = "contracts" solc = "0.8.33" evm_version = "cancun" @@ -18,13 +19,9 @@ optimizer_runs = 200 bytecode_hash = "none" cbor_metadata = false fs_permissions = [ - # Local proof artifacts, if someone drops them in; the OIDC flow tests - # self-skip when these files are absent. - { access = "read", path = "circuits/jwt_email/target" }, - { access = "read", path = "contracts/login/test/fixtures/oidc" }, # Google's real JWKS body, as served today, so the root list's parser is # tested against the document it will actually read rather than a shape # a test author remembered (see IdentityJwksRoots.t.sol). { access = "read", path = "contracts/identity/test/fixtures" }, - { access = "read", path = "out" }, # diamond deployer reads facet methodIdentifiers + { access = "read", path = "out" }, # the factory tests read creation code from the artifacts ] diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index cd092973..e97dcf2e 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -1,18 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; import {Script, console} from "forge-std/Script.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../contracts/login/Registry.sol"; -import {WalletFactory} from "../contracts/login/WalletFactory.sol"; -import {WebWallet} from "../contracts/login/WebWallet.sol"; -import {IBank} from "../contracts/transfer/bank/IBank.sol"; -import {BankDiamondDeployer} from "../contracts/transfer/script/BankDiamondDeployer.sol"; -import {Notary} from "../contracts/notary/Notary.sol"; -import {MockERC20} from "../contracts/transfer/MockERC20.sol"; -import {XHonkVerifier} from "../contracts/login/zk/XHonkVerifier.sol"; -import {XZkVerifier, IHonkVerifier} from "../contracts/login/zk/XZkVerifier.sol"; -import {IZkSessionVerifier} from "../contracts/login/zk/IZkSessionVerifier.sol"; import {IdentityNames} from "../contracts/identity/IdentityNames.sol"; import {NotaryService} from "../contracts/ceremony/NotaryService.sol"; import {INotaryService} from "../contracts/ceremony/INotaryService.sol"; @@ -21,18 +11,26 @@ import {IProofVerifier} from "../contracts/ceremony/IProofVerifier.sol"; import {HandleVectors} from "../contracts/identity/HandleVectors.sol"; import {IdentityJwksRoots} from "../contracts/identity/IdentityJwksRoots.sol"; -/// @notice Deploy full stack to any EVM chain. +/// @notice Deploy the identity stack to any EVM chain. +/// +/// Four UUPS proxies, in dependency order: the Notary Service every notarized +/// session is verified through, the Proof Verifier the naming system +/// dispatches claims through, the naming system itself with a keyspace per +/// platform, and the Google JWKS root list that pays the Notary Service for +/// each rotation. No Platform Verifier is registered here -- that needs the +/// ceremony circuit artifacts, which arrive with their own release. /// /// Usage: -/// forge script contracts/script/Deploy.s.sol \ +/// forge script script/Deploy.s.sol \ /// --rpc-url http://127.0.0.1:8545 \ /// --broadcast \ /// --private-key $PRIVATE_KEY /// -/// Optional env: -/// NOTARY_ADDRESS — notary signing key address. -/// BACKEND_ADDRESS — backend signing key address. -contract Deploy is Script, BankDiamondDeployer { +/// Env: +/// DEPLOYER_KEY / PRIVATE_KEY — deployer private key (one of the two). +/// NOTARY_ADDRESS — the first trusted notary signing key (default: deployer). +/// NOTARY_FEE_WEI — what one attestation verification costs (default: 0). +contract Deploy is Script { function run() external { string memory deployerKeyHex = vm.envOr("DEPLOYER_KEY", vm.envOr("PRIVATE_KEY", string(""))); require(bytes(deployerKeyHex).length > 0, "set DEPLOYER_KEY or PRIVATE_KEY"); @@ -43,137 +41,38 @@ contract Deploy is Script, BankDiamondDeployer { uint256 deployerKey = vm.parseUint(deployerKeyHex); address deployer = vm.addr(deployerKey); address notaryAddr = vm.envOr("NOTARY_ADDRESS", deployer); - address backendAddr = vm.envOr("BACKEND_ADDRESS", deployer); + uint256 notaryFee = vm.envOr("NOTARY_FEE_WEI", uint256(0)); vm.startBroadcast(deployerKey); - // 0. Notary (UUPS proxy) — the ONE notary-attestation verifier every - // other contract points at. Deployed first so its proxy address can - // be wired into every consumer's initialize. Key rotation is - // `notaryContract.setNotary`; a proof-system change is a UUPS - // upgrade of this proxy alone. - Notary notaryImpl = new Notary(); - Notary notaryContract = Notary( - address(new ERC1967Proxy(address(notaryImpl), abi.encodeCall(Notary.initialize, (deployer, notaryAddr)))) - ); - - // 1. WebWallet implementation (beacon target) - WebWallet walletImpl = new WebWallet(); - - // 2. WalletFactory (UUPS proxy) - WalletFactory factoryImpl = new WalletFactory(); - WalletFactory factory = WalletFactory( - address( - new ERC1967Proxy( - address(factoryImpl), - abi.encodeCall(WalletFactory.initialize, (deployer, address(walletImpl), address(0))) - ) - ) - ); - - // 4. Registry (UUPS proxy) — unified MPC + ZK registration - Registry registryImpl = new Registry(); - Registry registry = Registry( - address( - new ERC1967Proxy( - address(registryImpl), - abi.encodeCall( - Registry.initialize, (address(notaryContract), backendAddr, address(factory), deployer) - ) - ) - ) - ); - - // 5. Bank (EIP-2535 diamond) — facets + BankInit (seeds templates/prefixes). - // Same BANK_CONTRACT_ADDRESS surface as before; the diamond keeps every - // Bank function signature, so backend/frontend need only the new address. - IBank bank = IBank(deployBankDiamond(deployer, address(notaryContract), backendAddr, address(registry))); - - // 6. XHonkVerifier — single UltraHonk verifier for the merged - // X dual-session proof. Called from XZkVerifier. - XHonkVerifier xHonk = new XHonkVerifier(); - - // 7. MockERC20 dev token - MockERC20 devToken = new MockERC20("DEV Token", "DEV"); - - // 8. Wire factory <-> registry - factory.setRegistry(address(registry)); - - // 9. Register dev token in Bank. Name must include the "$" prefix — - // the comment parser keeps it (e.g. "$DEV") and the bot sends that - // exact string as tokenName_, so the on-chain name must match. - bank.registerToken("$DEV", address(devToken)); - - // 10. Mint dev tokens to deployer for testing (1M tokens) - devToken.mint(deployer, 1_000_000 ether); - - // 11. Deploy XZkVerifier (UUPS proxy) and register it with Registry - // under the "api.x.com" platform. One X app serves both the browser - // ZK flow and the backend OAuth flow, so the verifier's client_id is - // just $X_CLIENT_ID (must match the browser's NEXT_PUBLIC_X_CLIENT_ID). - string memory xClientIdStr = vm.envOr("X_CLIENT_ID", string("")); - address xZkVerifierAddr; - if (bytes(xClientIdStr).length > 0) { - XZkVerifier xZkImpl = new XZkVerifier(); - xZkVerifierAddr = address( - new ERC1967Proxy( - address(xZkImpl), - abi.encodeCall( - XZkVerifier.initialize, - ( - deployer, - address(notaryContract), - IHonkVerifier(address(xHonk)), - bytes(xClientIdStr), - "/2/users/me", - "\"username\":\"", - "api.x.com" - ) - ) - ) - ); - registry.setZkVerifier("api.x.com", IZkSessionVerifier(xZkVerifierAddr)); - } - - // 13. Seed supported platforms (mirrors deploy.rs platform_configs) - registry.setPlatform("api.x.com", "/2/users/me", "\"username\":\"", "\"id\":\"", "\""); - registry.setPlatform("api.github.com", "/user", "\"login\":\"", "\"id\":", ","); - registry.setPlatform("discord.com", "/api/users/@me", "\"username\":\"", "\"id\":\"", "\""); - registry.setPlatform("www.googleapis.com", "/oauth2/v2/userinfo", "\"email\": \"", "\"id\": \"", "\""); - - // 14. The naming system: one contract holding the names, and an - // adapter per platform that reads the proof that platform's login - // already produces. Nothing here changes how anybody logs in. - // - // The future allowance differs by platform because they report on - // different scales. A notary states wall-clock time, so an X or - // GitHub observation is never ahead; a Google claim carries the - // token's `exp`, roughly an hour past issuance, because the circuit - // exposes no `iat`. - IdentityNames namesImpl = new IdentityNames(); - address identityNamesAddr = - address(new ERC1967Proxy(address(namesImpl), abi.encodeCall(IdentityNames.initialize, (deployer)))); - IdentityNames names = IdentityNames(identityNamesAddr); - - // The ceremony core the Consumer dispatches through. Without it - // `proofVerifier` reads zero, `quoteClaim` calls address zero and every - // resolver reverts `UnknownPlatform` -- a deployment that can bind - // nothing. - // - // The Notary Service is separate from `Notary` above and must be: that - // one takes a caller-supplied digest, which REQ-COMMON-49 forbids. Both - // are trusted with the same signing key. + // 1. The Notary Service (UUPS proxy) -- the ONE place a notary + // attestation is authenticated, deployed first so its proxy address + // can be wired into every consumer's initialize. It derives the + // digest from the attested bytes itself (REQ-COMMON-49) and charges + // the Notary Fee. Key rotation is `setNotary`; a fee change is + // `setFee`; a proof-system change is a UUPS upgrade of this proxy + // alone. NotaryService notaryServiceImpl = new NotaryService(); address notaryServiceAddr = address( new ERC1967Proxy( - address(notaryServiceImpl), - abi.encodeCall(NotaryService.initialize, (deployer, notaryAddr, vm.envOr("NOTARY_FEE_WEI", uint256(0)))) + address(notaryServiceImpl), abi.encodeCall(NotaryService.initialize, (deployer, notaryAddr, notaryFee)) ) ); + // 2. The ceremony core the Consumer dispatches through. Without it + // `proofVerifier` reads zero, `quoteClaim` calls address zero and + // every resolver reverts `UnknownPlatform` -- a deployment that can + // bind nothing. CeremonyProofVerifier pvImpl = new CeremonyProofVerifier(); address proofVerifierAddr = address(new ERC1967Proxy(address(pvImpl), abi.encodeCall(CeremonyProofVerifier.initialize, (deployer)))); + + // 3. The naming system: one contract holding the names, dispatching + // every claim through the Proof Verifier above. + IdentityNames namesImpl = new IdentityNames(); + address identityNamesAddr = + address(new ERC1967Proxy(address(namesImpl), abi.encodeCall(IdentityNames.initialize, (deployer)))); + IdentityNames names = IdentityNames(identityNamesAddr); names.setProofVerifier(IProofVerifier(proofVerifierAddr)); // A keyspace per platform. Registering a Platform Verifier against a @@ -184,10 +83,11 @@ contract Deploy is Script, BankDiamondDeployer { _wireIdentityPlatform(names, HandleVectors.PLATFORM_GITHUB); _wireIdentityPlatform(names, HandleVectors.PLATFORM_GOOGLE); - // The JWKS root list is the naming system's own. Google's Platform - // Verifier reads the trusted moduli through it, and nothing is trusted - // until a notarized reading of Google's JWKS lands -- verified through - // the Notary Service above, like every other notarized session. + // 4. The JWKS root list is the naming system's own. Google's Platform + // Verifier reads the trusted moduli through it, and nothing is + // trusted until a notarized reading of Google's JWKS lands -- + // verified through the Notary Service above, like every other + // notarized session, and paying the same fee. IdentityJwksRoots rootsImpl = new IdentityJwksRoots(); address jwksRootsAddr = address( new ERC1967Proxy( @@ -200,17 +100,9 @@ contract Deploy is Script, BankDiamondDeployer { console.log("=== Deployment complete ==="); console.log("Deployer: ", deployer); - console.log("WEBWALLET_IMPL= ", address(walletImpl)); - console.log("WALLET_FACTORY_ADDRESS= ", address(factory)); - console.log("REGISTRY_CONTRACT_ADDRESS= ", address(registry)); - console.log("BANK_CONTRACT_ADDRESS= ", address(bank)); - console.log("DEV_TOKEN_ADDRESS= ", address(devToken)); - console.log("NOTARY_CONTRACT_ADDRESS= ", address(notaryContract)); - console.log("XHonkVerifier: ", address(xHonk)); - console.log("XZkVerifier: ", xZkVerifierAddr); - console.log("IDENTITY_NAMES_ADDRESS= ", identityNamesAddr); console.log("NOTARY_SERVICE_ADDRESS= ", notaryServiceAddr); console.log("CEREMONY_PROOF_VERIFIER_ADDRESS= ", proofVerifierAddr); + console.log("IDENTITY_NAMES_ADDRESS= ", identityNamesAddr); console.log("IDENTITY_JWKS_ROOTS_ADDRESS= ", jwksRootsAddr); console.log("NOTE: no Platform Verifier is registered yet. Add one with"); console.log(" CeremonyProofVerifier.setVerifier once the ceremony"); diff --git a/ts/packages/contracts/README.md b/ts/packages/contracts/README.md index 17092d33..66ad6190 100644 --- a/ts/packages/contracts/README.md +++ b/ts/packages/contracts/README.md @@ -22,22 +22,22 @@ pnpm add @libid/contracts viem ```ts import { createPublicClient, http } from 'viem' -import { registryAbi, bankAbi } from '@libid/contracts/abis' +import { identityJwksRootsAbi, identityNamesAbi } from '@libid/contracts/abis' const client = createPublicClient({ transport: http(RPC_URL) }) // Fully typed: viem infers the argument and return types from the ABI. -const wallet = await client.readContract({ - address: REGISTRY, - abi: registryAbi, - functionName: 'walletOf', - args: ['github', '583231'], +const owner = await client.readContract({ + address: IDENTITY_NAMES, + abi: identityNamesAbi, + functionName: 'resolveHandle', + args: [platformId, 'alice'], }) -const tokens = await client.readContract({ - address: BANK, - abi: bankAbi, - functionName: 'getRegisteredTokens', +const roots = await client.readContract({ + address: IDENTITY_JWKS_ROOTS, + abi: identityJwksRootsAbi, + functionName: 'currentRoots', }) ``` @@ -62,8 +62,9 @@ to compile rather than reverting on chain. Payable functions take `value` before their arguments and set it on the returned call: ```ts -const deposit = calls.bank.deposit(bank, 1_000n, 'x', 'alice', 'TIA', token, amount) -// { to: `0x…`, value: 1000n, data: `0x…` } +// A JWKS rotation pays the Notary Fee: read it with `quoteRotation` first. +const rotate = calls.identityJwksRoots.rotate(roots, fee, attestedData, proof) +// { to: `0x…`, value: fee, data: `0x…` } ``` ## Resolving a name diff --git a/ts/packages/contracts/scripts/codegen.mjs b/ts/packages/contracts/scripts/codegen.mjs index ae777f33..17b885bd 100644 --- a/ts/packages/contracts/scripts/codegen.mjs +++ b/ts/packages/contracts/scripts/codegen.mjs @@ -29,28 +29,17 @@ const callsDir = join(packageDir, 'src', 'calls') // Every contract the package ships an ABI for. `file` is the source unit under // solidity/out, `name` the contract inside it, `exportName` what a consumer -// imports. IBank exports as `bankAbi`: it is the facade the whole diamond -// answers to, and the facet ABIs exist for anyone wiring cuts by hand. +// imports. const contracts = [ - { file: 'Registry.sol', name: 'Registry', exportName: 'registryAbi' }, - { file: 'Notary.sol', name: 'Notary', exportName: 'notaryAbi' }, - { file: 'WalletFactory.sol', name: 'WalletFactory', exportName: 'walletFactoryAbi' }, - { file: 'WebWallet.sol', name: 'WebWallet', exportName: 'webWalletAbi' }, - { file: 'IBank.sol', name: 'IBank', exportName: 'bankAbi' }, - { file: 'AdminFacet.sol', name: 'AdminFacet', exportName: 'adminFacetAbi' }, - { file: 'VaultFacet.sol', name: 'VaultFacet', exportName: 'vaultFacetAbi' }, - { file: 'TransferFacet.sol', name: 'TransferFacet', exportName: 'transferFacetAbi' }, - { file: 'DiamondLoupeFacet.sol', name: 'DiamondLoupeFacet', exportName: 'diamondLoupeFacetAbi' }, { file: 'IdentityNames.sol', name: 'IdentityNames', exportName: 'identityNamesAbi' }, { file: 'IdentityJwksRoots.sol', name: 'IdentityJwksRoots', exportName: 'identityJwksRootsAbi' }, - { file: 'XZkVerifier.sol', name: 'XZkVerifier', exportName: 'xZkVerifierAbi' }, + { file: 'NotaryService.sol', name: 'NotaryService', exportName: 'notaryServiceAbi' }, { - file: 'GoogleOidcVerifier.sol', - name: 'GoogleOidcVerifier', - exportName: 'googleOidcVerifierAbi', + file: 'CeremonyProofVerifier.sol', + name: 'CeremonyProofVerifier', + exportName: 'ceremonyProofVerifierAbi', }, { file: 'LibidFactory.sol', name: 'LibidFactory', exportName: 'libidFactoryAbi' }, - { file: 'MockERC20.sol', name: 'MockERC20', exportName: 'mockErc20Abi' }, { file: 'WTIA9.sol', name: 'WTIA9', exportName: 'wtia9Abi' }, ] diff --git a/ts/packages/contracts/src/calls.test.ts b/ts/packages/contracts/src/calls.test.ts index d33a40b9..8c3dafc7 100644 --- a/ts/packages/contracts/src/calls.test.ts +++ b/ts/packages/contracts/src/calls.test.ts @@ -1,12 +1,17 @@ import { decodeFunctionData } from 'viem' import { describe, expect, it } from 'vitest' -import { bankAbi } from './abis/bank.js' +import { identityJwksRootsAbi } from './abis/identityJwksRoots.js' import { identityNamesAbi } from './abis/identityNames.js' import { calls } from './index.js' const NAMES = '0x00000000000000000000000000000000000000aa' as const +const ROOTS = '0x00000000000000000000000000000000000000cc' as const const PLATFORM = `0x${'11'.repeat(32)}` as const +// Opaque to the builder: the contract decodes the record and the Notary +// Service checks the proof, so any bytes round-trip through the ABI here. +const ATTESTED = `0x${'a7'.repeat(40)}` as const +const PROOF = `0x${'b8'.repeat(65)}` as const describe('generated call builders', () => { it('encodes a call the ABI decodes back to the same arguments', () => { @@ -30,26 +35,20 @@ describe('generated call builders', () => { }) it('takes value before the arguments on a payable function', () => { - const call = calls.bank.deposit( - NAMES, - 7n, - 'x', - 'alice', - 'TIA', - '0x00000000000000000000000000000000000000bb', - 3n, - ) + // A rotation pays the Notary Fee, so the value is the part a caller must + // not be able to forget: it comes before the attested bytes and the proof. + const call = calls.identityJwksRoots.rotate(ROOTS, 7n, ATTESTED, PROOF) expect(call.value).toBe(7n) - expect(call.to).toBe(NAMES) - expect(decodeFunctionData({ abi: bankAbi, data: call.data })).toEqual({ - functionName: 'deposit', - args: ['x', 'alice', 'TIA', '0x00000000000000000000000000000000000000bb', 3n], + expect(call.to).toBe(ROOTS) + expect(decodeFunctionData({ abi: identityJwksRootsAbi, data: call.data })).toEqual({ + functionName: 'rotate', + args: [ATTESTED, PROOF], }) }) it('namespaces by contract, so colliding names stay distinct', () => { // `initialize` is on almost every contract; the two must not be one export. - expect(calls.identityNames.initialize).not.toBe(calls.registry.initialize) + expect(calls.notaryService.initialize).not.toBe(calls.identityNames.initialize) }) }) From 6def7330ae38cebab4417c684f5ae59489a0c4c8 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 03:28:09 +0100 Subject: [PATCH 03/15] fix(identity): the Host pin refuses bare-LF and folded lines, and counts the head only The JWKS root list's whole safety argument is that a session to www.googleapis.com carried exactly one `Host: www.googleapis.com`: googleapis.com serves many virtual hosts under one certificate, and only the Host header pins which backend answered. The count was CRLF-anchored over normalized bytes, and normalization keeps CR and LF -- so a second `Host` whose preceding line ended in a bare LF was never counted, and an obsolete line fold (`\r\n host:`) was counted as the legitimate one. Confirmed live: Google's front end honours the bare-LF `Host: storage.googleapis.com` and routes to the storage backend, while the contract saw one CRLF `Host`, the right one, and installed the keys. The bearer-header count in CeremonyAttestation already guards against both shapes before counting. Those two scans are now one internal helper, `requireCrlfLineEndings`, shared the way `normalizeHeaderBytes` is: what a scan refuses decides what a count can miss, so one implementation per security-relevant scan rather than one per caller. `_requireJwksRequest` runs it right after the request line, in the bearer path's order. The `Host` count also stops at the request's head boundary. A `Host` after the blank line is body bytes to the server, whatever a count over the whole direction would make of it; a request whose head never ends is refused with `NoHeadBoundary`, the same error the response side raises. No new errors, so the ABI the bindings vendor is unchanged. Tests: the live bare-LF request, the fold (alone and paired with a bare-LF second host), a Host only in the body, and a head that never ends. All four passed against the previous contract. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .../ceremony/CeremonyAttestation.sol | 63 +++++++++++++------ .../contracts/identity/IdentityJwksRoots.sol | 24 ++++++- .../identity/test/IdentityJwksRoots.t.sol | 51 +++++++++++++++ 3 files changed, 117 insertions(+), 21 deletions(-) diff --git a/solidity/contracts/ceremony/CeremonyAttestation.sol b/solidity/contracts/ceremony/CeremonyAttestation.sol index b70e84a7..070b1eb8 100644 --- a/solidity/contracts/ceremony/CeremonyAttestation.sol +++ b/solidity/contracts/ceremony/CeremonyAttestation.sol @@ -202,27 +202,8 @@ library CeremonyAttestation { requireExactCoverage(block_, length); - // Obsolete line folding is illegal in HTTP/1.1 and defeats the needle: - // `authorization:\r\n Bearer x` normalizes to - // `authorization:\r\nbearer`, because normalization strips the space - // but keeps the CRLF the fold introduced. The header is then never - // counted, and a server honouring the fold authenticates with it. bytes memory revealed = concatRevealed(block_); - for (uint256 i = 0; i + 2 < revealed.length; ++i) { - if (revealed[i] == 0x0d && revealed[i + 1] == 0x0a && (revealed[i + 2] == 0x20 || revealed[i + 2] == 0x09)) - { - revert ObsoleteLineFold(i); - } - } - // Every LF must be part of a CRLF. Otherwise - // `...\nauthorization: Bearer \r\n` starts a header line the - // CRLF-anchored needle never counts, while a lenient platform parser - // honours it. - for (uint256 i = 0; i < revealed.length; ++i) { - if (revealed[i] == 0x0a && (i == 0 || revealed[i - 1] != 0x0d)) { - revert BareLineFeed(i); - } - } + requireCrlfLineEndings(revealed); // Counted over the CONCATENATION, not per range. // @@ -256,6 +237,48 @@ library CeremonyAttestation { } } + /// @notice Every line ending in the revealed request bytes is a CRLF, and + /// no line continues the one before it. + /// + /// @dev A CRLF-anchored header count is only as good as the line + /// structure it anchors to, and HTTP/1.1 parsers in the wild accept + /// two shapes the needle cannot see. + /// + /// Obsolete line folding is illegal in HTTP/1.1 and defeats the + /// needle: `authorization:\r\n Bearer x` normalizes to + /// `authorization:\r\nbearer`, because normalization strips the space + /// but keeps the CRLF the fold introduced. The header is then never + /// counted, and a server honouring the fold authenticates with it. + /// + /// Every LF must be part of a CRLF. Otherwise + /// `...\nauthorization: Bearer \r\n` starts a header line the + /// CRLF-anchored needle never counts, while a lenient platform parser + /// honours it. + /// + /// `internal` for the same reason `normalizeHeaderBytes` is: what + /// this refuses decides what a header count can miss, so it is one + /// implementation shared by every count -- the bearer header here and + /// the JWKS root list's `Host` -- rather than one per caller. The + /// `Host` count learned this the hard way: Google's front end honours + /// a bare-LF `Host: storage.googleapis.com` and routes to the storage + /// backend, while a needle count sees only the CRLF one before it. + /// + /// Runs BEFORE the count, over the raw bytes: normalization keeps CR + /// and LF, so the offsets it reports are transcript offsets. + function requireCrlfLineEndings(bytes memory revealed) internal pure { + for (uint256 i = 0; i + 2 < revealed.length; ++i) { + if (revealed[i] == 0x0d && revealed[i + 1] == 0x0a && (revealed[i + 2] == 0x20 || revealed[i + 2] == 0x09)) + { + revert ObsoleteLineFold(i); + } + } + for (uint256 i = 0; i < revealed.length; ++i) { + if (revealed[i] == 0x0a && (i == 0 || revealed[i - 1] != 0x0d)) { + revert BareLineFeed(i); + } + } + } + /// @notice Lowercase ASCII and drop every space and horizontal tab, keeping /// CR and LF (REQ-COMMON-39). /// diff --git a/solidity/contracts/identity/IdentityJwksRoots.sol b/solidity/contracts/identity/IdentityJwksRoots.sol index 62963fc3..50eae93f 100644 --- a/solidity/contracts/identity/IdentityJwksRoots.sol +++ b/solidity/contracts/identity/IdentityJwksRoots.sol @@ -163,6 +163,9 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad error WrongHost(); /// @dev Not `HTTP/1.1 200 `. error WrongStatusLine(); + /// @dev A direction with no blank line ending its head. Raised for the + /// request too: its `Host` count runs over the head alone, and a + /// request whose head never ends has no head to count in. error NoHeadBoundary(); error BadChunkFraming(); /// @dev The declared `Content-Length` is not the body the transcript @@ -388,16 +391,35 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// @dev The request must be the keeper's, to Google's JWKS endpoint, on /// Google's own host. + /// + /// The `Host` count is the whole of the virtual-host pin, so it gets + /// every guard the bearer-header count gets, in the same order. The + /// notary vouches that these bytes crossed a TLS session to + /// www.googleapis.com and nothing about their HTTP shape; the prover + /// chose them. Google's front end honours a `Host` line that only a + /// bare LF separates from the one before it, and routes to that + /// backend -- so a count that anchors on CRLF must first insist the + /// request has no other kind of line ending. function _requireJwksRequest(CeremonyAttestation.DirectionBlock memory block_, uint32 length) private pure { bytes memory sent = _wholeDirection(block_, length); if (!_startsWith(sent, REQUEST_LINE)) revert WrongRequestLine(); + CeremonyAttestation.requireCrlfLineEndings(sent); + + // Only the head names a host. A `Host` after the blank line is body + // bytes to the server, whatever the count would make of it, so the + // count stops where the server stops reading headers. The head keeps + // the CRLF that closes its last line, so a line-anchored needle + // matches that line too. + uint256 boundary = _indexOf(sent, HEAD_BOUNDARY, 0); + if (boundary == type(uint256).max) revert NoHeadBoundary(); + bytes memory head = _slice(sent, 0, boundary + 2); // Counted over normalized bytes, so a second `Host` cannot hide // behind case or whitespace (REQ-COMMON-39's rule, applied to a // different header). Exactly one: with two, which the backend // honoured is the backend's business, not something this contract // can know. - bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(sent); + bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(head); (uint256 count, uint256 at) = _find(normalized, HOST_NEEDLE); if (count != 1) revert NotOneHostHeader(count); diff --git a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol index 13c1dd8f..8fff36ef 100644 --- a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol +++ b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol @@ -798,6 +798,57 @@ contract IdentityJwksRootsTest is Test { _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 2)); } + /// The notary vouches for the bytes, not their HTTP shape. Google's front + /// end honours a second `Host` that a bare LF separates from the line + /// before it -- confirmed live: this request is answered by the storage + /// backend -- while a CRLF-anchored count sees one `Host`, the right one. + /// The bare LF is refused before anything is counted, at its offset. + function test_refusesASecondHostHeaderBehindABareLineFeed() public { + bytes memory request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", + "x-a: b\nhost: storage.googleapis.com\r\n", + "host: www.googleapis.com\r\n", + "connection: close\r\n\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(CeremonyAttestation.BareLineFeed.selector, 37)); + } + + /// An obsolete line fold normalizes to `\r\nhost:` -- the fold's space is + /// stripped, its CRLF kept -- so a folded continuation would be counted as + /// the one legitimate `Host` and pair with a bare-LF second one. The fold + /// is refused first, at the CRLF that starts it. + function test_refusesAnObsoleteLineFoldInTheRequest() public { + bytes memory request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", "x-a: b\r\n host: www.googleapis.com\r\n", "connection: close\r\n\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(CeremonyAttestation.ObsoleteLineFold.selector, 37)); + + // The pairing the fold enables: a folded right host over a bare-LF + // wrong one. The fold scan runs first and names the fold. + request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", + "x-a: b\r\n host: www.googleapis.com\r\n", + "x-b: c\nhost: storage.googleapis.com\r\n", + "connection: close\r\n\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(CeremonyAttestation.ObsoleteLineFold.selector, 37)); + } + + /// The server reads hosts from the head. A `Host` after the blank line + /// is body bytes, and does not satisfy the count. + function test_aHostInTheRequestBodyDoesNotCount() public { + bytes memory request = abi.encodePacked( + "GET /oauth2/v3/certs HTTP/1.1\r\n", "connection: close\r\n\r\n", "host: www.googleapis.com\r\n" + ); + _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 0)); + } + + /// A head that never ends has no head to count in. + function test_refusesARequestWhoseHeadNeverEnds() public { + bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com\r\n"; + _refuses(_withRequest(request), IdentityJwksRoots.NoHeadBoundary.selector); + } + function test_refusesARequestWithoutAHostHeader() public { bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nconnection: close\r\n\r\n"; _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 0)); From 604b42a4582d8e92840d7918c48f777401e7d0de Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 03:37:03 +0100 Subject: [PATCH 04/15] fix(identity): refuse an empty key set, equal-time swaps and doubled framing headers Three shapes the review found the list still accepted, each closed with the test that shows it. A reading whose key set is empty paid the fee, moved the freshest-reading mark and wrote nothing; it now reverts EmptyKeySet. Two readings dated the same second could disagree about a kid, and the second one won: it swapped the key and retired the one Google still signs with. The provenance floor is strict now, so equal evidence leaves the entry alone whatever it carries -- the rule equal evidence gets everywhere else in the naming system. A response head carrying two Content-Length lines, two Transfer-Encoding lines, or one of each was framed by whichever copy the parser met first. The header the body is located by gets the rule every other revealed read gets: a delimiter that matches twice is refused, not chosen from. A coding other than chunked is refused too, rather than read as a raw body. The upgrade-safety test lands a rotation before upgrading, so what has to survive is real state in the namespaced slots and not the empty list initialize left behind. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .../ceremony/test/UpgradeSafety.t.sol | 47 ++++++++++++++- .../contracts/identity/IdentityJwksRoots.sol | 52 +++++++++++++---- .../identity/test/IdentityJwksRoots.t.sol | 58 ++++++++++++++++++- 3 files changed, 144 insertions(+), 13 deletions(-) diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index cb17ab60..5d6162a6 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -21,6 +21,7 @@ import {HandleVectors} from "../../identity/HandleVectors.sol"; import {HandleNormalizer} from "../../identity/HandleNormalizer.sol"; import {IdentityNodes} from "../../identity/IdentityNodes.sol"; import {StubPlatformVerifier} from "../../identity/test/StubPlatformVerifier.sol"; +import {AttestationBuilder} from "./AttestationBuilder.sol"; contract RHonk is IHonkVerifier { function verify(bytes calldata, bytes32[] calldata) external pure returns (bool) { @@ -254,6 +255,23 @@ contract UpgradeSafetyTest is Test { vm.expectRevert(Initializable.InvalidInitialization.selector); r.initialize(OWNER, ns); + // A rotation lands BEFORE the upgrade, so what has to survive is real + // state in the namespaced slots rather than the empty list initialize + // left behind. + uint256 notaryKey = 0xA11CE5; + vm.prank(OWNER); + NotaryService(address(ns)).setNotary(vm.addr(notaryKey), true); + bytes memory attested = _jwksReading(uint64(vm.getBlockTimestamp())); + (uint8 v, bytes32 sigR, bytes32 sigS) = + vm.sign(notaryKey, keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(attested)))); + vm.deal(address(this), 1 ether); + r.rotate{value: 7}(attested, abi.encodePacked(sigR, sigS, v)); + bytes32 kidHash = keccak256("k"); + bytes32 modulus = r.modulusOfKid(kidHash); + assertTrue(modulus != bytes32(0), "the rotation landed"); + uint256 stamp = r.rotatedAtKid(kidHash); + uint256 expiry = r.expiresAtKid(kidHash); + IdentityJwksRoots impl2 = new IdentityJwksRoots(); _expectNotOwner(); r.upgradeToAndCall(address(impl2), ""); @@ -262,10 +280,37 @@ contract UpgradeSafetyTest is Test { assertEq(_implOf(address(r)), address(impl2)); assertEq(r.notaryService(), address(ns)); assertEq(r.quoteRotation(), 7); - assertTrue(r.needsRotation()); assertEq(r.owner(), OWNER); + assertEq(r.modulusOfKid(kidHash), modulus, "the rotated key was lost"); + assertEq(r.trustedHashExpiresAt(modulus), expiry, "the trust stamp was lost"); + assertEq(r.rotatedAtKid(kidHash), stamp, "the provenance floor was lost"); + assertEq(r.freshestObservedAt(), stamp); + assertEq(r.currentRoots().length, 1, "the tracked set was lost"); + assertFalse(r.needsRotation()); + } + + /// One notarized reading of Google's JWKS carrying one key: the request + /// and the response revealed whole, nothing committed. + function _jwksReading(uint64 createdAt) internal pure returns (bytes memory) { + bytes memory sent = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com\r\nconnection: close\r\n\r\n"; + bytes memory body = abi.encodePacked('{"keys":[{"kid":"k","n":"', JWKS_MODULUS, '"}]}'); + bytes memory received = + abi.encodePacked("HTTP/1.1 200 OK\r\ncontent-length: ", vm.toString(body.length), "\r\n\r\n", body); + return AttestationBuilder.encode(keccak256("www.googleapis.com"), createdAt, _whole(sent), _whole(received)); } + function _whole(bytes memory transcript) internal pure returns (AttestationBuilder.Direction memory) { + return AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: transcript})), + commitments: AttestationBuilder.none(), + length: uint32(transcript.length) + }); + } + + /// A 2048-bit modulus in base64url (the keeper's own test vector). + string internal constant JWKS_MODULUS = + "BAsSGSAnLjU8Q0pRWF9mbXR7gomQl56lrLO6wcjP1t3k6_L5BQwTGiEoLzY9REtSWWBnbnV8g4qRmJ-mrbS7wsnQ197l7PP6Bg0UGyIpMDc-RUxTWmFob3Z9hIuSmaCnrrW8w8rR2N_m7fT7Bw4VHCMqMTg_Rk1UW2JpcHd-hYyTmqGor7a9xMvS2eDn7vUBCA8WHSQrMjlAR05VXGNqcXh_ho2Um6KpsLe-xczT2uHo7_YCCRAXHiUsMzpBSE9WXWRrcnmAh46VnKOqsbi_xs3U2-Lp8PcDChEYHyYtNDtCSVBXXmVsc3qBiI-WnaSrsrnAx87V3OPq8fgECxIZIA"; + // ─── IdentityNames ────────────────────────────────────────────── IdentityNames names; diff --git a/solidity/contracts/identity/IdentityJwksRoots.sol b/solidity/contracts/identity/IdentityJwksRoots.sol index 50eae93f..81508a17 100644 --- a/solidity/contracts/identity/IdentityJwksRoots.sol +++ b/solidity/contracts/identity/IdentityJwksRoots.sol @@ -118,6 +118,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad bytes private constant EXPECTED_HOST = "www.googleapis.com"; bytes private constant STATUS_LINE = "HTTP/1.1 200 "; bytes private constant HEAD_BOUNDARY = "\r\n\r\n"; + bytes private constant TRANSFER_ENCODING_NEEDLE = "\r\ntransfer-encoding:"; bytes private constant CHUNKED_NEEDLE = "\r\ntransfer-encoding:chunked\r\n"; bytes private constant CONTENT_LENGTH_NEEDLE = "\r\ncontent-length:"; bytes private constant KEYS_TOKEN = '"keys"'; @@ -142,6 +143,10 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad error ZeroAddress(); error TooManyKids(); + /// @dev A reading whose key set is empty says nothing about what Google + /// publishes; applying it would move `freshestObservedAt` while + /// writing no key and emitting nothing. Refused rather than charged. + error EmptyKeySet(); /// @dev Exact value, both ways: the Notary Fee is forwarded whole, so there /// is no overpayment to refund and nothing captured in transit. error WrongValue(uint256 required, uint256 provided); @@ -172,6 +177,13 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// carries. A value that is not a decimal declares no length any /// body can match. error ContentLengthMismatch(uint256 declared, uint256 found); + /// @dev A framing header appears more than once. The header the body is + /// located by gets the rule every other read gets (REQ-COMMON-19A): + /// a delimiter matching twice is refused, not chosen from. + error DuplicateFramingHeader(string name, uint256 count); + /// @dev Both `Transfer-Encoding` and `Content-Length` are present, so two + /// parsers could frame two bodies. + error ConflictingFraming(); /// @dev The member is absent, or present but not in the shape read here: /// `keys` must be an array, `kid` and `n` must be strings. error MissingMember(string name); @@ -454,11 +466,25 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad body = _slice(received, boundary + 4, received.length); bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(head); - (uint256 chunked,) = _find(normalized, CHUNKED_NEEDLE); - if (chunked != 0) return _dechunk(body); - - (uint256 declaredLength, uint256 at) = _find(normalized, CONTENT_LENGTH_NEEDLE); - if (declaredLength != 0) { + (uint256 codings,) = _find(normalized, TRANSFER_ENCODING_NEEDLE); + (uint256 lengths, uint256 at) = _find(normalized, CONTENT_LENGTH_NEEDLE); + // Exactly one framing header, of one kind. Two of either, or one of + // each, is a message two parsers could frame two ways -- the + // request-smuggling shape -- and a field read from revealed bytes is + // refused when its delimiter matches twice, not read from whichever + // copy the server happened to honour. + if (codings > 1) revert DuplicateFramingHeader("transfer-encoding", codings); + if (lengths > 1) revert DuplicateFramingHeader("content-length", lengths); + if (codings != 0 && lengths != 0) revert ConflictingFraming(); + + if (codings != 0) { + // The one coding this reads. Any other frames a body this + // contract cannot locate. + (uint256 chunked,) = _find(normalized, CHUNKED_NEEDLE); + if (chunked != 1) revert BadChunkFraming(); + return _dechunk(body); + } + if (lengths != 0) { uint256 declared = _decimal(normalized, at + CONTENT_LENGTH_NEEDLE.length, body.length); if (declared != body.length) revert ContentLengthMismatch(declared, body.length); } @@ -566,6 +592,8 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad at = close + 1; } + if (keys == 0) revert EmptyKeySet(); + at = _ws(body, at); if (at >= body.length || body[at] != "}") revert TrailingBytes(); if (_ws(body, at + 1) != body.length) revert TrailingBytes(); @@ -640,13 +668,15 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad // front-runner from bricking the honest keeper's batch by landing one // claim from it first, and skipping (never applying) keeps an older // reading from rolling a kid back or re-stamping its TTL. - if (provenAt < $.rotatedAtKid[kidHash]) return; + // + // Strictly later, so equal evidence leaves the entry alone whatever it + // carries: a resubmission of the applied reading is a no-op that + // re-stamps nothing and emits nothing, and a second reading dated the + // same second cannot swap the kid's key and retire the one Google + // still signs with. That is the rule for equal evidence everywhere in + // the naming system (REQ-COMMON-25A). + if (provenAt <= $.rotatedAtKid[kidHash]) return; bytes32 previous = $.modulusOfKid[kidHash]; - // A byte-identical resubmission of the applied reading is a no-op: - // nothing is written, nothing is emitted, and in particular the TTL is - // NOT re-stamped -- so spamming the same reading neither grows state - // nor stretches trust. - if (provenAt == $.rotatedAtKid[kidHash] && previous == modulusHash) return; $.rotatedAtKid[kidHash] = provenAt; // The key this kid used to carry stops being trusted NOW, rather than diff --git a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol index 8fff36ef..41fb2100 100644 --- a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol +++ b/solidity/contracts/identity/test/IdentityJwksRoots.t.sol @@ -44,7 +44,8 @@ contract IdentityJwksRootsTest is Test { "BAsSGSAnLjU8Q0pRWF9mbXR7gomQl56lrLO6wcjP1t3k6_L5BQwTGiEoLzY9REtSWWBnbnV8g4qRmJ-mrbS7wsnQ197l7PP6Bg0UGyIpMDc-RUxTWmFob3Z9hIuSmaCnrrW8w8rR2N_m7fT7Bw4VHCMqMTg_Rk1UW2JpcHd-hYyTmqGor7a9xMvS2eDn7vUBCA8WHSQrMjlAR05VXGNqcXh_ho2Um6KpsLe-xczT2uHo7_YCCRAXHiUsMzpBSE9WXWRrcnmAh46VnKOqsbi_xs3U2-Lp8PcDChEYHyYtNDtCSVBXXmVsc3qBiI-WnaSrsrnAx87V3OPq8fgECxIZIA"; bytes32 constant KEEPER_MODULUS_HASH = 0xc5c1457ba4adf8bb84324e8595bf883639aa7ebd7aa7e7626a0b51f90e6838bc; - /// The request the keeper's prover sends, byte for byte. + /// The request the keeper's prover sends. The User-Agent value is not + /// read on chain, so the version it carries is immaterial. bytes constant REQUEST = "GET /oauth2/v3/certs HTTP/1.1\r\n" "host: www.googleapis.com\r\n" "connection: close\r\n" "accept: application/json\r\n" "user-agent: libid-keeper/0.1.0\r\n" "\r\n"; bytes constant STATUS_OK = "HTTP/1.1 200 OK\r\n"; @@ -1080,6 +1081,61 @@ contract IdentityJwksRootsTest is Test { _refuses(_withBody(body), IdentityJwksRoots.InvalidB64Length.selector); } + /// A reading with no keys says nothing about what Google publishes. It + /// would pay the fee, move `freshestObservedAt` and emit nothing. + function test_refusesAnEmptyKeySet() public { + _refuses(_withBody('{"keys":[]}'), IdentityJwksRoots.EmptyKeySet.selector); + _refuses(_withBody('{"keys": [ ]}'), IdentityJwksRoots.EmptyKeySet.selector); + } + + /// Two readings dated the same second cannot disagree about a kid: the + /// second is ignored whatever it carries, so equal evidence never swaps a + /// key and retires the one Google still signs with. + function test_aReadingDatedTheSameSecondCannotSwapAKey() public { + uint64 at = _now(); + _rotate(_readingAt(_oneKey("kid-1", "one"), at)); + bytes32 kidHash = keccak256("kid-1"); + bytes32 installed = roots.modulusOfKid(kidHash); + uint256 expiry = roots.expiresAtKid(kidHash); + + vm.recordLogs(); + _rotate(_readingAt(_oneKey("kid-1", "two"), at)); // must not revert, must not apply + + assertEq(roots.modulusOfKid(kidHash), installed, "the same second swapped the key"); + assertEq(roots.trustedHashExpiresAt(installed), expiry, "the installed key lost trust"); + assertEq(roots.rotatedAtKid(kidHash), at, "the stamp moved"); + assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + } + + /// The header the body is located by gets the rule every other read + /// gets: a delimiter that matches twice is refused, not chosen from. + function test_refusesDuplicateOrConflictingFramingHeaders() public { + bytes memory body = _oneKey("kid-1", "one"); + bytes memory length = abi.encodePacked("content-length: ", vm.toString(body.length), "\r\n"); + _refuses( + _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, length, length, "\r\n", body)), + abi.encodeWithSelector(IdentityJwksRoots.DuplicateFramingHeader.selector, "content-length", 2) + ); + bytes memory chunked = "transfer-encoding: chunked\r\n"; + _refuses( + _withResponse( + abi.encodePacked( + STATUS_OK, CONTENT_TYPE, chunked, "Transfer-Encoding: identity\r\n", "\r\n", _chunks(body, 64) + ) + ), + abi.encodeWithSelector(IdentityJwksRoots.DuplicateFramingHeader.selector, "transfer-encoding", 2) + ); + _refuses( + _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, chunked, length, "\r\n", _chunks(body, 64))), + IdentityJwksRoots.ConflictingFraming.selector + ); + // A coding other than chunked frames a body this contract cannot locate. + _refuses( + _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, "transfer-encoding: gzip\r\n", "\r\n", body)), + IdentityJwksRoots.BadChunkFraming.selector + ); + } + // ─── Helpers ──────────────────────────────────────────────────── /// Read through the cheatcode rather than `block.timestamp`: under via-IR From 5c057b865ef32bf551b1b0130e31b3f800a4a602 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 11:30:46 +0100 Subject: [PATCH 05/15] refactor!: remove the wallet products instead of archiving them The previous commit moved Registry, WebWallet, WalletFactory, XZkVerifier, GoogleOidcVerifier, the Bank diamond and the legacy Notary under solidity/archived/ as frozen reference. A tree that is never compiled, tested or shipped still has to be read around, kept out of every forge root, and explained in two places; git history and tag v0.6.0 already hold the code, and this change's diff is where a reader finds what was removed and how it worked. So the archive goes: the directory, the README layout entry, and the foundry.toml note that kept it outside the source roots. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- README.md | 2 - solidity/archived/README.md | 45 - solidity/archived/login/IRegistry.sol | 27 - solidity/archived/login/Registry.sol | 743 ----- solidity/archived/login/SnapshotProxy.sol | 31 - solidity/archived/login/WalletFactory.sol | 113 - solidity/archived/login/WebWallet.sol | 389 --- .../login/oidc/GoogleOidcVerifier.sol | 793 ----- .../archived/login/oidc/IOidcVerifier.sol | 38 - solidity/archived/login/oidc/Verifier.sol | 2461 --------------- .../login/oidc/script/DeployOidc.s.sol | 71 - solidity/archived/login/test/Registry.t.sol | 1068 ------- .../archived/login/test/TlsLinkGriefPoC.t.sol | 228 -- .../archived/login/test/WalletFactory.t.sol | 92 - solidity/archived/login/test/WebWallet.t.sol | 723 ----- .../login/test/fixtures/oidc/jwks-proof.json | 37 - .../test/fixtures/oidc/jwks-snapshot.json | 20 - .../test/oidc/GoogleOidcVerifierFlow.t.sol | 616 ---- solidity/archived/login/test/oidc/README.md | 50 - .../test/oidc/RegistryOidcLinkFlow.t.sol | 171 -- .../archived/login/zk/IZkSessionVerifier.sol | 54 - solidity/archived/login/zk/XHonkVerifier.sol | 2461 --------------- solidity/archived/login/zk/XZkVerifier.sol | 561 ---- .../login/zk/test/XZkVerifierAllowlist.t.sol | 208 -- .../login/zk/test/XZkVerifierBindings.t.sol | 658 ---- .../login/zk/test/XZkVerifierFlow.t.sol | 255 -- solidity/archived/notary/INotary.sol | 24 - solidity/archived/notary/Notary.sol | 77 - .../archived/notary/test/DeployNotary.sol | 14 - solidity/archived/notary/test/Notary.t.sol | 157 - solidity/archived/transfer/MockERC20.sol | 14 - .../archived/transfer/bank/BankErrors.sol | 79 - solidity/archived/transfer/bank/BankInit.sol | 63 - .../archived/transfer/bank/BankModifiers.sol | 30 - solidity/archived/transfer/bank/BankTypes.sol | 47 - solidity/archived/transfer/bank/IBank.sol | 98 - .../transfer/bank/facets/AdminFacet.sol | 186 -- .../transfer/bank/facets/TransferFacet.sol | 333 -- .../transfer/bank/facets/VaultFacet.sol | 137 - .../transfer/bank/libraries/LibEscrow.sol | 96 - .../transfer/bank/libraries/LibProof.sol | 97 - .../transfer/bank/libraries/LibRecipient.sol | 124 - .../transfer/bank/libraries/LibString.sol | 146 - .../transfer/bank/libraries/LibTemplate.sol | 192 -- .../transfer/bank/storage/LibCoreStorage.sol | 28 - .../bank/storage/LibEscrowStorage.sol | 25 - .../bank/storage/LibTemplateStorage.sol | 33 - .../transfer/bank/storage/LibTokenStorage.sol | 22 - .../archived/transfer/diamond/Diamond.sol | 46 - .../diamond/facets/DiamondCutFacet.sol | 14 - .../diamond/facets/DiamondLoupeFacet.sol | 41 - .../diamond/facets/OwnershipFacet.sol | 19 - .../diamond/interfaces/IDiamondCut.sol | 27 - .../diamond/interfaces/IDiamondLoupe.sol | 23 - .../transfer/diamond/interfaces/IERC173.sol | 13 - .../transfer/diamond/libraries/LibDiamond.sol | 196 -- .../transfer/script/BankDiamondDeployer.sol | 68 - .../transfer/script/DeployBankDiamond.s.sol | 38 - .../archived/transfer/test/BalanceViews.t.sol | 97 - .../archived/transfer/test/BankDiamond.t.sol | 130 - .../transfer/test/BankDiamondHonor.t.sol | 273 -- .../archived/transfer/test/DollarToken.t.sol | 45 - .../archived/transfer/test/EdgeCases.t.sol | 590 ---- .../archived/transfer/test/EscrowById.t.sol | 146 - .../transfer/test/ImplicitTemplate.t.sol | 168 - .../archived/transfer/test/Integration.t.sol | 2720 ----------------- .../test/IntegrationNewFeatures.t.sol | 130 - .../transfer/test/LibStringGuards.t.sol | 30 - .../archived/transfer/test/NativeAsset.t.sol | 460 --- .../archived/transfer/test/PausableFlow.t.sol | 157 - .../transfer/test/RegistryBankRecycle.t.sol | 34 - .../transfer/test/TemplateMatching.t.sol | 372 --- .../transfer/test/TransferWithin.t.sol | 364 --- solidity/foundry.toml | 3 +- 74 files changed, 1 insertion(+), 20140 deletions(-) delete mode 100644 solidity/archived/README.md delete mode 100644 solidity/archived/login/IRegistry.sol delete mode 100644 solidity/archived/login/Registry.sol delete mode 100644 solidity/archived/login/SnapshotProxy.sol delete mode 100644 solidity/archived/login/WalletFactory.sol delete mode 100644 solidity/archived/login/WebWallet.sol delete mode 100644 solidity/archived/login/oidc/GoogleOidcVerifier.sol delete mode 100644 solidity/archived/login/oidc/IOidcVerifier.sol delete mode 100644 solidity/archived/login/oidc/Verifier.sol delete mode 100644 solidity/archived/login/oidc/script/DeployOidc.s.sol delete mode 100644 solidity/archived/login/test/Registry.t.sol delete mode 100644 solidity/archived/login/test/TlsLinkGriefPoC.t.sol delete mode 100644 solidity/archived/login/test/WalletFactory.t.sol delete mode 100644 solidity/archived/login/test/WebWallet.t.sol delete mode 100644 solidity/archived/login/test/fixtures/oidc/jwks-proof.json delete mode 100644 solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json delete mode 100644 solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol delete mode 100644 solidity/archived/login/test/oidc/README.md delete mode 100644 solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol delete mode 100644 solidity/archived/login/zk/IZkSessionVerifier.sol delete mode 100644 solidity/archived/login/zk/XHonkVerifier.sol delete mode 100644 solidity/archived/login/zk/XZkVerifier.sol delete mode 100644 solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol delete mode 100644 solidity/archived/login/zk/test/XZkVerifierBindings.t.sol delete mode 100644 solidity/archived/login/zk/test/XZkVerifierFlow.t.sol delete mode 100644 solidity/archived/notary/INotary.sol delete mode 100644 solidity/archived/notary/Notary.sol delete mode 100644 solidity/archived/notary/test/DeployNotary.sol delete mode 100644 solidity/archived/notary/test/Notary.t.sol delete mode 100644 solidity/archived/transfer/MockERC20.sol delete mode 100644 solidity/archived/transfer/bank/BankErrors.sol delete mode 100644 solidity/archived/transfer/bank/BankInit.sol delete mode 100644 solidity/archived/transfer/bank/BankModifiers.sol delete mode 100644 solidity/archived/transfer/bank/BankTypes.sol delete mode 100644 solidity/archived/transfer/bank/IBank.sol delete mode 100644 solidity/archived/transfer/bank/facets/AdminFacet.sol delete mode 100644 solidity/archived/transfer/bank/facets/TransferFacet.sol delete mode 100644 solidity/archived/transfer/bank/facets/VaultFacet.sol delete mode 100644 solidity/archived/transfer/bank/libraries/LibEscrow.sol delete mode 100644 solidity/archived/transfer/bank/libraries/LibProof.sol delete mode 100644 solidity/archived/transfer/bank/libraries/LibRecipient.sol delete mode 100644 solidity/archived/transfer/bank/libraries/LibString.sol delete mode 100644 solidity/archived/transfer/bank/libraries/LibTemplate.sol delete mode 100644 solidity/archived/transfer/bank/storage/LibCoreStorage.sol delete mode 100644 solidity/archived/transfer/bank/storage/LibEscrowStorage.sol delete mode 100644 solidity/archived/transfer/bank/storage/LibTemplateStorage.sol delete mode 100644 solidity/archived/transfer/bank/storage/LibTokenStorage.sol delete mode 100644 solidity/archived/transfer/diamond/Diamond.sol delete mode 100644 solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol delete mode 100644 solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol delete mode 100644 solidity/archived/transfer/diamond/facets/OwnershipFacet.sol delete mode 100644 solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol delete mode 100644 solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol delete mode 100644 solidity/archived/transfer/diamond/interfaces/IERC173.sol delete mode 100644 solidity/archived/transfer/diamond/libraries/LibDiamond.sol delete mode 100644 solidity/archived/transfer/script/BankDiamondDeployer.sol delete mode 100644 solidity/archived/transfer/script/DeployBankDiamond.s.sol delete mode 100644 solidity/archived/transfer/test/BalanceViews.t.sol delete mode 100644 solidity/archived/transfer/test/BankDiamond.t.sol delete mode 100644 solidity/archived/transfer/test/BankDiamondHonor.t.sol delete mode 100644 solidity/archived/transfer/test/DollarToken.t.sol delete mode 100644 solidity/archived/transfer/test/EdgeCases.t.sol delete mode 100644 solidity/archived/transfer/test/EscrowById.t.sol delete mode 100644 solidity/archived/transfer/test/ImplicitTemplate.t.sol delete mode 100644 solidity/archived/transfer/test/Integration.t.sol delete mode 100644 solidity/archived/transfer/test/IntegrationNewFeatures.t.sol delete mode 100644 solidity/archived/transfer/test/LibStringGuards.t.sol delete mode 100644 solidity/archived/transfer/test/NativeAsset.t.sol delete mode 100644 solidity/archived/transfer/test/PausableFlow.t.sol delete mode 100644 solidity/archived/transfer/test/RegistryBankRecycle.t.sol delete mode 100644 solidity/archived/transfer/test/TemplateMatching.t.sol delete mode 100644 solidity/archived/transfer/test/TransferWithin.t.sol diff --git a/README.md b/README.md index 28608f70..6eedab42 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ solidity/ # Foundry project root identity/ # IdentityNames, IdentityJwksRoots, handle normalization factory/ # LibidFactory: deterministic CREATE3 deployment WTIA9.sol # wrapped TIA - archived/ # the frozen wallet and bank products: reference only, - # never compiled (see archived/README.md) script/Deploy.s.sol lib/ # git submodules (openzeppelin, forge-std) rust/contracts/ # libid-contracts crate: alloy bindings + embedded artifacts diff --git a/solidity/archived/README.md b/solidity/archived/README.md deleted file mode 100644 index f4cb5d0f..00000000 --- a/solidity/archived/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Archived contracts - -Reference only. Nothing under this directory is compiled, tested, shipped, -or deployed by this repository. - -## What is here - -- `login/` — the wallet product's login stack: `Registry` (the one place a - notarized or zero-knowledge session became a platform → wallet binding), - `WebWallet` and `WalletFactory` (the smart-account wallets it minted), the - `XZkVerifier` ZK session verifier and the `GoogleOidcVerifier` OIDC verifier, - together with the two bb-generated UltraHonk verifiers they called - (`oidc/Verifier.sol`, `zk/XHonkVerifier.sol`), their tests, fixtures and - deploy scripts. -- `transfer/` — the transfer product: the `Bank` EIP-2535 diamond (facets, - init, libraries, storage), its deployer, the `MockERC20` dev token, and its - tests. -- `notary/` — the legacy `Notary`: a single-key attestation verifier that - took a caller-computed digest. Everything in `contracts/` now verifies - notary attestations through `ceremony/NotaryService`, which derives the - digest from the attested bytes itself (REQ-COMMON-49) and charges the - Notary Fee. - -They are kept, rather than deleted, as a record of how a full product was -built on notarized sessions: the login stack is the worked example of turning -a session into an on-chain binding, and the bank is the worked example of a -consumer spending one. Git history has every version of them; this directory -holds the last. - -## Status - -Frozen as of the commit that moved them here. They last built and passed -their tests at tag `v0.6.0`, against that tag's `foundry.toml` and -submodules. Nothing regenerates them: the bb-generated verifiers are not -re-derived from a circuits release, no Rust bindings or TypeScript ABIs are -produced for them, and CI does not look at them. - -This directory sits outside forge's `src`, `test` and `script` roots, so -`forge build` never sees it. Moving a file back under `contracts/` is what -would make it compile again — and it would need to be brought up to the -current bases (ERC-7201 namespaced storage + UUPS + Ownable2Step) and to -`NotaryService` before it could be deployed alongside the rest. - -The wallet product is not deployed by this repository any more. -`script/Deploy.s.sol` deploys the identity stack only. diff --git a/solidity/archived/login/IRegistry.sol b/solidity/archived/login/IRegistry.sol deleted file mode 100644 index f775b582..00000000 --- a/solidity/archived/login/IRegistry.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title IRegistry — maps (platform, handle) → wallet address. -interface IRegistry { - /// @notice Resolve a platform handle to its wallet contract address. - /// @return The wallet address, or address(0) if not registered. - function resolve(string calldata platform, string calldata handle) external view returns (address); - - /// @notice Resolve a platform id to its wallet — authoritative, no handle indirection. - /// @return The wallet address, or address(0) if not registered. - function resolveById(string calldata platform, string calldata id) external view returns (address); - - /// @notice The immutable id a handle currently points to ("" if unbound). - function handleHint(string calldata platform, string calldata handle) external view returns (string memory); - - /// @notice Get all (platform, handle) pairs linked to a wallet. - function getHandles(address wallet) external view returns (string[] memory platforms, string[] memory handles); - - /// @notice Get all (platform, userId) pairs linked to a wallet — the - /// immutable identity key (handle is a mutable label). "" for - /// identities registered before the id refactor. - function getUserIds(address wallet) external view returns (string[] memory platforms, string[] memory userIds); - - /// @notice Emitted when a handle is registered or updated. - event HandleRegistered(string platform, string handle, address indexed owner); -} diff --git a/solidity/archived/login/Registry.sol b/solidity/archived/login/Registry.sol deleted file mode 100644 index 37a1f9fc..00000000 --- a/solidity/archived/login/Registry.sol +++ /dev/null @@ -1,743 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import {IRegistry} from "./IRegistry.sol"; -import {WalletFactory} from "./WalletFactory.sol"; -import {WebWallet} from "./WebWallet.sol"; -import {IZkSessionVerifier} from "./zk/IZkSessionVerifier.sol"; -import {IOidcVerifier} from "./oidc/IOidcVerifier.sol"; -import {INotary} from "../notary/INotary.sol"; - -/// @title Registry — session registration and identity linking with 2-of-2 proof verification. -contract Registry is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, PausableUpgradeable, IRegistry { - using ECDSA for bytes32; - - // ─── Proof verification state ────────────────────────────────── - - /// @notice Tolerance for proof timestamps that land slightly ahead of - /// `block.timestamp`. Eden's block head typically trails wall - /// clock by a handful of seconds, so a freshly notarized proof - /// would otherwise revert with `FutureProof`. - uint256 public constant CLOCK_SKEW_GRACE = 5 minutes; - - struct HandleEntry { - string platform; - string handle; - string userId; // immutable platform id; "" for pre-refactor entries - } - - /// @notice Per-domain platform configuration for on-chain verification. - struct PlatformConfig { - string endpoint; // expected API endpoint path, e.g. "/user" - string handlePrefix; // JSON prefix including quotes/colon/whitespace, e.g. '"login":"' or '"email": "' - string idPrefix; // JSON prefix of the immutable id. Empty disables id-binding. - string idSuffix; // JSON suffix anchoring the id's end: '"' for quoted (X), ',' for a bare number (GitHub) - } - - struct IdMeta { - string handle; // current display handle for this id - uint256 verifiedAt; // block.timestamp of last id-handle proof - } - - /// @custom:storage-location erc7201:dyaka.storage.Registry - struct RegistryStorage { - INotary notaryContract; // the shared Notary contract (verifies notary attestations) - address backend; // backend signer - WalletFactory walletFactory; - /// Replay protection: sessionAddress to already used (flat, globally unique). - mapping(address => bool) usedSessionKeys; - /// wallet contract to array of (platform, handle, id) tuples. - mapping(address => HandleEntry[]) contract_to_handles; - /// domain to platform config. A platform is enabled if its endpoint is not empty. - mapping(string => PlatformConfig) platformConfigs; - /// Per-platform ZK verifier. Each verifier owns its circuit, notary key - /// and platform bindings. The Registry dispatches by platform string - /// and tracks only session keys and nullifiers. - mapping(string => IZkSessionVerifier) zkVerifierOf; - /// Replay protection: the domain-wrapped bearer nullifier from the - /// verifier. The Registry consumes it one time. - mapping(bytes32 => bool) usedBearerNullifiers; - /// domain to OIDC verifier. A platform supports OIDC if its verifier is - /// set. The owner manages these. The OIDC path is the alternative to - /// the TLS-notary flow and gives the same claim shape. - mapping(string => IOidcVerifier) oidcVerifierOf; - mapping(string => mapping(string => address)) walletOf; // [platform][id] to wallet (TRUTH) - mapping(string => mapping(string => IdMeta)) idMeta; // [platform][id] to meta (display) - mapping(string => mapping(string => string)) handleHint; // [platform][handle] to id (re-pointable) - } - - // keccak256(abi.encode(uint256(keccak256("dyaka.storage.Registry")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant REGISTRY_STORAGE = 0x42c0aa58641382e077ceeffba10b73e1be97f010be3e8a2a89d2914f8b201b00; - - /// @dev All state of this contract is under one namespaced root. The state - /// cannot collide with the ERC-7201 namespaces of the OpenZeppelin - /// upgradeable base contracts. An upgrade can append a field without - /// slot arithmetic. Append only. Do not reorder or remove a field. - function _s() private pure returns (RegistryStorage storage $) { - assembly { - $.slot := REGISTRY_STORAGE - } - } - - // ─── Storage reads (the ABI the public variables gave) ───────── - - /// @notice The Notary contract this Registry routes attestation checks through. - function notaryContract() external view returns (address) { - return address(_s().notaryContract); - } - - /// @notice The current notary signer, read through the Notary contract. - /// Kept so observers keep the read they had when the key was local. - function notary() external view returns (address) { - return _s().notaryContract.notary(); - } - - function backend() external view returns (address) { - return _s().backend; - } - - function walletFactory() external view returns (WalletFactory) { - return _s().walletFactory; - } - - function usedSessionKeys(address sessionAddr) external view returns (bool) { - return _s().usedSessionKeys[sessionAddr]; - } - - function contract_to_handles(address wallet, uint256 index) - external - view - returns (string memory platform, string memory handle, string memory userId) - { - HandleEntry storage entry = _s().contract_to_handles[wallet][index]; - return (entry.platform, entry.handle, entry.userId); - } - - function platformConfigs(string calldata domain) - external - view - returns (string memory endpoint, string memory handlePrefix, string memory idPrefix, string memory idSuffix) - { - PlatformConfig storage cfg = _s().platformConfigs[domain]; - return (cfg.endpoint, cfg.handlePrefix, cfg.idPrefix, cfg.idSuffix); - } - - function zkVerifierOf(string calldata domain) external view returns (IZkSessionVerifier) { - return _s().zkVerifierOf[domain]; - } - - function usedBearerNullifiers(bytes32 nullifier) external view returns (bool) { - return _s().usedBearerNullifiers[nullifier]; - } - - function oidcVerifierOf(string calldata domain) external view returns (IOidcVerifier) { - return _s().oidcVerifierOf[domain]; - } - - function walletOf(string calldata platform, string calldata userId) external view returns (address) { - return _s().walletOf[platform][userId]; - } - - function idMeta(string calldata platform, string calldata userId) - external - view - returns (string memory handle, uint256 verifiedAt) - { - IdMeta storage meta = _s().idMeta[platform][userId]; - return (meta.handle, meta.verifiedAt); - } - - function handleHint(string calldata platform, string calldata handle) external view returns (string memory) { - return _s().handleHint[platform][handle]; - } - - /// @notice Convenience read for ZK-domain support. - function isZkDomainSupported(string calldata domain) public view returns (bool) { - return address(_s().zkVerifierOf[domain]) != address(0); - } - - // ─── Proof struct (Merkle paths included) ────────────────────── - - struct FullTlsProof { - bytes notarySignature; - bytes backendSignature; - /// Session keypair public address generated by the prover's browser; - /// becomes the session key registered against the wallet. Signed - /// into the _s().backend digest, so attackers cannot rebind it. - address userAddress; - /// Bound to _s().backend sig. `address(0)` for register_session, - /// `msg.sender` for linkIdentity. - address walletAddress; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] usernamePath; - bytes32[] endpointPath; - bytes32[] idPath; // path for the "id":"" recv leaf; empty when userId is "" - } - - // ─── Events ──────────────────────────────────────────────────── - - event SessionRegistered(string platform, string handle, address indexed wallet, address sessionAddr); - event IdentityLinked(string platform, string handle, address indexed wallet); - /// Emitted when an id's current handle changes (rename). Lets the indexer - /// refresh the off-chain label keyed by the immutable id. - event HandleChanged(string platform, string userId, string handle); - - // ─── Errors ──────────────────────────────────────────────────── - - error InvalidMerkleProof(); - error SessionKeyAlreadyUsed(); - error InvalidNotarySignature(); - error InvalidBackendSignature(); - error StaleProof(); - error FutureProof(); - error MerklePathTooLong(); - error NotRegisteredWallet(); - error IdentityAlreadyLinked(); - error UserIdRequired(); - error UnknownId(); - error UnsupportedPlatform(); - error WrongEndpoint(); - error ZkVerifierNotSet(); - error BearerNullifierUsed(); - error ProofExpired(); - error WrongWalletAddr(); - error JwtExpired(); - error OidcVerifierNotSet(); - error LinkWalletMismatch(); - - // ─── Initializer ────────────────────────────────────────────── - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @param _notaryContract The shared Notary contract (INotary) every - /// attestation check routes through. - function initialize(address _notaryContract, address _backend, address _walletFactory, address _owner) - external - initializer - { - __Ownable_init(_owner); - __Ownable2Step_init(); - __UUPSUpgradeable_init(); - __Pausable_init(); - require(_notaryContract != address(0), "zero notary contract"); - require(_backend != address(0), "zero _s().backend"); - require(_walletFactory != address(0), "zero factory"); - _s().notaryContract = INotary(_notaryContract); - _s().backend = _backend; - _s().walletFactory = WalletFactory(_walletFactory); - } - - /// @notice Pause state-changing entrypoints. Owner-only emergency stop and - /// migration-cutover gate (covers the permissionless ZK/OIDC paths a - /// _s().backend-pause can't reach). Admin/owner fns stay callable. - function pause() external onlyOwner { - _pause(); - } - - /// @notice Resume after a pause. - function unpause() external onlyOwner { - _unpause(); - } - - // ─── Admin ───────────────────────────────────────────────────── - - function setBackend(address _backend) external onlyOwner { - require(_backend != address(0), "zero _s().backend"); - _s().backend = _backend; - } - - /// @notice Register or update a supported platform. `idPrefix`/`idSuffix` - /// enable id-binding (X: '"id":"' + '"'; GitHub: '"id":' + ','); pass - /// empty idPrefix to leave it off. idSuffix anchors the id's end. - function setPlatform( - string calldata domain, - string calldata endpoint, - string calldata handlePrefix, - string calldata idPrefix, - string calldata idSuffix - ) external onlyOwner { - require(bytes(domain).length > 0, "empty domain"); - require(bytes(endpoint).length > 0, "empty endpoint"); - require(bytes(handlePrefix).length > 0, "empty key"); - _s().platformConfigs[domain] = PlatformConfig(endpoint, handlePrefix, idPrefix, idSuffix); - } - - /// @notice Remove a platform (clears config, disabling registrations). - function removePlatform(string calldata domain) external onlyOwner { - delete _s().platformConfigs[domain]; - } - - /// @notice Set or clear the ZK verifier for a platform. Pass - /// `IZkSessionVerifier(address(0))` to disable. - function setZkVerifier(string calldata domain, IZkSessionVerifier verifier) external onlyOwner { - require(bytes(domain).length > 0, "empty domain"); - _s().zkVerifierOf[domain] = verifier; - } - - /// @notice Set or clear the OIDC verifier for a platform. Pass - /// `IOidcVerifier(address(0))` to disable. - function setOidcVerifier(string calldata platform, IOidcVerifier verifier) external onlyOwner { - require(bytes(platform).length > 0, "empty platform"); - _s().oidcVerifierOf[platform] = verifier; - } - - // ─── Session registration ────────────────────────────────────── - - /// @notice Register a session address for a social identity. The session - /// key is `proof.userAddress` (notary + backend signed). No - /// loose `sessionAddr` parameter -- a previous version of this - /// function let a mempool front-runner substitute the session - /// key without invalidating the signatures. - function register_session( - FullTlsProof calldata proof, - string calldata domain, - string calldata username, - string calldata userId, - string calldata endpoint - ) external whenNotPaused { - // Session key is proof.userAddress (notary + backend signed); no loose - // sessionAddr param (anti front-run). New identity → walletAddress must - // be the zero sentinel so the factory deploys the wallet. - if (proof.walletAddress != address(0)) revert WrongWalletAddr(); - - _verifyProof(proof, domain, username, userId, endpoint); - - _registerHandle(domain, username, userId, proof.userAddress); - } - - // ─── ZK session registration (dispatcher) ────────────────────── - - /// @notice Register a session via the platform's configured - /// `IZkSessionVerifier`. The verifier owns all platform- - /// specific logic (circuit, notary, attestation shape); the - /// Registry only enforces dispatch + global session-key and - /// nullifier replay protection, then deploys the wallet and - /// registers the session. - function register_session_zk(string calldata platform, bytes calldata zkProof) external whenNotPaused { - IZkSessionVerifier verifier = _s().zkVerifierOf[platform]; - if (address(verifier) == address(0)) revert ZkVerifierNotSet(); - - ( - string memory handle, - address sessionAddr, - address walletAddress, - uint256 expiresAt, - bytes32 nullifier, - string memory userId - ) = verifier.verifyAndExtract(zkProof); - - // Factory deploys the wallet: PI wallet must be the zero sentinel. - if (walletAddress != address(0)) revert WrongWalletAddr(); - if (block.timestamp >= expiresAt) revert ProofExpired(); - _consumeNullifier(nullifier); - _registerHandle(platform, handle, userId, sessionAddr); - } - - /// Link a ZK identity to the *calling* wallet. PI walletAddress must - /// equal msg.sender so a leaked proof cannot be replayed from another - /// wallet -- the binding is committed inside the ZK proof. - function link_identity_zk(string calldata platform, bytes calldata zkProof) external whenNotPaused { - IZkSessionVerifier verifier = _s().zkVerifierOf[platform]; - if (address(verifier) == address(0)) revert ZkVerifierNotSet(); - - ( - string memory handle, - address sessionAddr, - address walletAddress, - uint256 expiresAt, - bytes32 nullifier, - string memory userId - ) = verifier.verifyAndExtract(zkProof); - - if (walletAddress != msg.sender) revert WrongWalletAddr(); - if (block.timestamp >= expiresAt) revert ProofExpired(); - _consumeNullifier(nullifier); - _linkHandle(platform, handle, userId, sessionAddr); - } - - // ─── Shared internals ────────────────────────────────────────── - - /// One-shot consume of a ZK bearer nullifier (replay guard). - function _consumeNullifier(bytes32 nullifier) internal { - if (_s().usedBearerNullifiers[nullifier]) revert BearerNullifierUsed(); - _s().usedBearerNullifiers[nullifier] = true; - } - - /// @dev Point `handle` at `userId` as the id's current label. Clears the id's - /// PREVIOUS handle hint (rename) so an abandoned handle stops resolving here — - /// but only if that hint still points to this id (not recycled to another). - function _bindHandle(string memory platform, string memory handle, string memory userId) internal { - string memory prevHandle = _s().idMeta[platform][userId].handle; - bool renamed = bytes(prevHandle).length != 0 && keccak256(bytes(prevHandle)) != keccak256(bytes(handle)); - if (renamed && keccak256(bytes(_s().handleHint[platform][prevHandle])) == keccak256(bytes(userId))) { - delete _s().handleHint[platform][prevHandle]; - } - _s().idMeta[platform][userId] = IdMeta(handle, block.timestamp); - _s().handleHint[platform][handle] = userId; - if (renamed) emit HandleChanged(platform, userId, handle); - } - - /// Deploy-or-find the wallet for `(platform, handle)` and register the - /// session key. Used by every `register_session_*` (new-identity) path. - function _registerHandle(string memory platform, string memory handle, string memory userId, address sessionAddr) - internal - { - // Escrow is keyed by the immutable platform id — never the mutable - // handle. No handle-key fallback; a registration without an id is - // rejected (legacy handle-keyed wallets are migrated separately). - if (bytes(userId).length == 0) revert UserIdRequired(); - require(sessionAddr != address(0), "zero session"); - if (_s().usedSessionKeys[sessionAddr]) revert SessionKeyAlreadyUsed(); - _s().usedSessionKeys[sessionAddr] = true; - - // ── (platform, id) is the truth ── - address wallet = _s().walletOf[platform][userId]; - if (wallet == address(0)) { - wallet = _s().walletFactory.deploy_web_wallet(platform, handle, userId); - _s().walletOf[platform][userId] = wallet; - _s().contract_to_handles[wallet].push(HandleEntry(platform, handle, userId)); - // New id→wallet binding only; returning users emit SessionRegistered. - emit HandleRegistered(platform, handle, wallet); - } - _bindHandle(platform, handle, userId); - WebWallet(payable(wallet)).register_session(platform, handle, userId, sessionAddr); - emit SessionRegistered(platform, handle, wallet, sessionAddr); - } - - /// Attach `(platform, handle)` to the calling registered wallet and - /// register the session key. Used by every `link_*` path; the caller - /// must have verified the proof is bound to `msg.sender`. - function _linkHandle(string memory platform, string memory handle, string memory userId, address sessionAddr) - internal - { - _linkHandleTo(msg.sender, platform, handle, userId, sessionAddr); - } - - /// Link an identity to an EXPLICIT wallet rather than `msg.sender`. - /// - /// A caller passing a wallet other than `msg.sender` must have already - /// established that the wallet authorized the link — e.g. a signature from - /// one of the wallet's existing session keys carried inside the proof, which - /// is what would make such a transaction sponsorable. No such caller exists - /// today; `_linkHandle` passes `msg.sender`. - function _linkHandleTo( - address wallet, - string memory platform, - string memory handle, - string memory userId, - address sessionAddr - ) internal { - if (_s().contract_to_handles[wallet].length == 0) revert NotRegisteredWallet(); - if (bytes(userId).length == 0) revert UserIdRequired(); - require(sessionAddr != address(0), "zero session"); - if (_s().usedSessionKeys[sessionAddr]) revert SessionKeyAlreadyUsed(); - _s().usedSessionKeys[sessionAddr] = true; - - // ── an id maps to exactly one wallet ── - address linked = _s().walletOf[platform][userId]; - if (linked != address(0) && linked != wallet) revert IdentityAlreadyLinked(); - // Only append a HandleEntry the first time this id links to this wallet. - // Re-linking the same (platform, userId) just refreshes the label/session; - // a duplicate entry would break the one-entry-per-(platform,userId) invariant. - if (linked != wallet) { - _s().walletOf[platform][userId] = wallet; - _s().contract_to_handles[wallet].push(HandleEntry(platform, handle, userId)); - } - _bindHandle(platform, handle, userId); - WebWallet(payable(wallet)).register_session(platform, handle, userId, sessionAddr); - emit IdentityLinked(platform, handle, wallet); - } - - // ─── OIDC session registration ───────────────────────────────── - - /// @notice OIDC variant of `register_session`. The caller passes an - /// opaque OIDC proof (bytes); the configured `IOidcVerifier` - /// for `platform` validates it and extracts the user's - /// identity, session key, and JWT expiry. Downstream wallet - /// deploy + event emission is identical to the TLSN flow, so - /// the indexer treats OIDC registrations the same as any - /// other handle registration. - /// - /// @param platform Platform domain string, e.g. `"www.googleapis.com"`. - /// @param oidcProof Opaque payload understood by the configured - /// verifier (for `GoogleOidcVerifier`: ABI-encoded - /// `UserProof` carrying the Honk proof, public - /// inputs, and the email + session-key plaintexts). - function register_session_oidc(string calldata platform, bytes calldata oidcProof) external whenNotPaused { - IOidcVerifier verifier = _s().oidcVerifierOf[platform]; - if (address(verifier) == address(0)) revert OidcVerifierNotSet(); - - (string memory handle, address sessionAddr, uint256 jwtExp, string memory userId) = - verifier.verifyAndExtract(oidcProof); - - // The verifier already enforces JWT freshness against its own - // CLOCK_SKEW_GRACE, but the Registry re-checks against its own - // tolerance so all session-key bindings share the same liveness - // contract regardless of which provider validated the proof. - if (jwtExp + CLOCK_SKEW_GRACE <= block.timestamp) revert JwtExpired(); - - // Escrow keys on the immutable JWT `sub`, not the mutable email handle. - _registerHandle(platform, handle, userId, sessionAddr); - } - - // ─── OIDC identity linking ───────────────────────────────────── - - /// @notice Link an OIDC identity (e.g. Gmail) to the *existing* wallet - /// that calls this function, instead of deploying a new one - /// (the `register_session_oidc` behaviour). - /// - /// Front-run safe: the OIDC `nonce` is bound to the calling - /// wallet — `verifyAndExtract` returns it and we require it to - /// equal `msg.sender`. The nonce lives inside the provider-signed - /// JWT, so a stolen proof cannot be replayed by another wallet. - /// - /// @param platform Platform domain string, e.g. `"www.googleapis.com"`. - /// @param oidcProof Verifier proof whose JWT `nonce` is this wallet's address. - /// @param sessionAddr Session key to register for the linked handle. - function link_identity_oidc(string calldata platform, bytes calldata oidcProof, address sessionAddr) - external - whenNotPaused - { - // Caller must be a registered WebWallet (its session key authorized - // this via WebWallet.execute) — same consent gate as linkIdentity. - if (_s().contract_to_handles[msg.sender].length == 0) revert NotRegisteredWallet(); - - IOidcVerifier verifier = _s().oidcVerifierOf[platform]; - if (address(verifier) == address(0)) revert OidcVerifierNotSet(); - - (string memory handle, address boundWallet, uint256 jwtExp, string memory userId) = - verifier.verifyAndExtract(oidcProof); - - // The JWT `nonce` is bound to the linking wallet (returned as the - // session/bound address), so the proof is only usable by that wallet - // — closes the front-run window. - if (boundWallet != msg.sender) revert LinkWalletMismatch(); - if (jwtExp + CLOCK_SKEW_GRACE <= block.timestamp) revert JwtExpired(); - - // Escrow keys on the immutable JWT `sub`. `sessionAddr` is the session - // key the caller wants registered for the linked handle. - _linkHandle(platform, handle, userId, sessionAddr); - } - - // ─── Identity linking ────────────────────────────────────────── - - /// Link an additional social identity to msg.sender. Backend-signed - /// `proof.walletAddress` must equal `msg.sender` to prevent mempool replay. - function linkIdentity( - string calldata platform, - string calldata handle, - string calldata userId, - FullTlsProof calldata proof, - string calldata endpoint - ) external whenNotPaused { - // Cheap fail-fast (also enforced by _linkHandle). - if (_s().contract_to_handles[msg.sender].length == 0) revert NotRegisteredWallet(); - if (proof.walletAddress != msg.sender) revert WrongWalletAddr(); - - _verifyProof(proof, platform, handle, userId, endpoint); - - // Delegate to the shared helper (guards + binding live there). - _linkHandle(platform, handle, userId, proof.userAddress); - } - - /// @notice Re-point a handle's hint to its id with a fresh attestation. Permissionless. - function refreshHandleHint( - string calldata platform, - string calldata id, - string calldata handle, - FullTlsProof calldata proof, - string calldata endpoint - ) external whenNotPaused { - _verifyProof(proof, platform, handle, id, endpoint); - if (_s().walletOf[platform][id] == address(0)) revert UnknownId(); - _bindHandle(platform, handle, id); - } - - /// @inheritdoc IRegistry - /// @dev A handle resolves through the id that it points to now. The old - /// `handle_to_contract` fallback is removed. No code has written that - /// mapping since handles stopped being the truth, so the fallback - /// always returned zero. This namespace is append-only, and a dead - /// slot in it stays forever. - function resolve(string calldata platform, string calldata handle) external view returns (address) { - string storage id = _s().handleHint[platform][handle]; - if (bytes(id).length == 0) return address(0); - return _s().walletOf[platform][id]; - } - - /// @inheritdoc IRegistry - function resolveById(string calldata platform, string calldata id) external view returns (address) { - return _s().walletOf[platform][id]; - } - - // ─── Views ───────────────────────────────────────────────────── - - /// @notice Return the platform config for a domain. Empty endpoint means unconfigured. - function getPlatform(string calldata domain) - external - view - returns (string memory endpoint, string memory handlePrefix) - { - PlatformConfig storage pc = _s().platformConfigs[domain]; - return (pc.endpoint, pc.handlePrefix); - } - - function getHandles(address wallet) external view returns (string[] memory platforms, string[] memory handles) { - HandleEntry[] storage entries = _s().contract_to_handles[wallet]; - uint256 len = entries.length; - platforms = new string[](len); - handles = new string[](len); - for (uint256 i = 0; i < len; i++) { - platforms[i] = entries[i].platform; - // Frozen handle — matches the legacy handle-keyed slot kept for - // pre-id consumers. For a rename-safe DISPLAY handle - // use getCurrentHandles (idMeta-backed). - handles[i] = entries[i].handle; - } - } - - /// @notice Like getHandles but returns each id's CURRENT handle (idMeta), - /// rename-safe — for display, never for claim-key computation. - function getCurrentHandles(address wallet) - external - view - returns (string[] memory platforms, string[] memory handles) - { - HandleEntry[] storage entries = _s().contract_to_handles[wallet]; - uint256 len = entries.length; - platforms = new string[](len); - handles = new string[](len); - for (uint256 i = 0; i < len; i++) { - platforms[i] = entries[i].platform; - string memory current = _s().idMeta[entries[i].platform][entries[i].userId].handle; - handles[i] = bytes(current).length > 0 ? current : entries[i].handle; - } - } - - /// @notice Platform user-ids linked to `wallet`, parallel to getHandles. - /// The authoritative claim key for id-keyed accounting. "" for legacy entries. - function getUserIds(address wallet) external view returns (string[] memory platforms, string[] memory userIds) { - HandleEntry[] storage entries = _s().contract_to_handles[wallet]; - uint256 len = entries.length; - platforms = new string[](len); - userIds = new string[](len); - for (uint256 i = 0; i < len; i++) { - platforms[i] = entries[i].platform; - userIds[i] = entries[i].userId; - } - } - - // ─── Internal: full proof verification ───────────────────────── - - /// @notice Verify timestamps, platform/endpoint, domain hash, 2-of-2 sigs, - /// and the 4 merkle leaves. Shared by register/link/refresh. - function _verifyProof( - FullTlsProof calldata proof, - string calldata domain, - string calldata handle, - string calldata userId, - string calldata endpoint - ) internal view returns (PlatformConfig storage pc) { - if (proof.timestamp > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); - if (block.timestamp > proof.timestamp + 1 hours) revert StaleProof(); - - pc = _s().platformConfigs[domain]; - if (bytes(pc.endpoint).length == 0) revert UnsupportedPlatform(); - if (keccak256(bytes(endpoint)) != keccak256(bytes(pc.endpoint))) revert WrongEndpoint(); - - require(keccak256(bytes(domain)) == proof.domainHash, "domain hash mismatch"); - - _verifyNotarySignature(proof); - _verifyBackendSignature(proof); - - _verifyLeaf(proof.domainPath, proof.transcriptRoot, "domain:", bytes(domain)); - _verifyUsernameLeaf(proof.usernamePath, proof.transcriptRoot, pc.handlePrefix, handle); - _verifyLeaf(proof.endpointPath, proof.transcriptRoot, "endpoint:", bytes(endpoint)); - _verifyId(proof, pc, userId); - } - - // ─── Internal: signature verification ────────────────────────── - - function _verifyNotarySignature(FullTlsProof calldata proof) internal view { - // Domain-separated by (chainId, this contract). The notary mirror - // is `crypto::compute_notary_digest` in the original monorepo's - // auth crate. The Notary contract owns the attestation check - // itself (EIP-191 + recover + signer compare today). - bytes32 proofDigest = keccak256( - abi.encode( - block.chainid, - address(this), - proof.domainHash, - proof.clientRandom, - proof.serverRandom, - keccak256(proof.serverEphemeralKey), - proof.transcriptRoot, - proof.timestamp - ) - ); - if (!_s().notaryContract.verify(proofDigest, proof.notarySignature)) revert InvalidNotarySignature(); - } - - function _verifyBackendSignature(FullTlsProof calldata proof) internal view { - bytes32 backendDigest = - keccak256(abi.encode(proof.userAddress, proof.walletAddress, proof.transcriptRoot, proof.timestamp)); - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", backendDigest)); - address signer = ethHash.recover(proof.backendSignature); - if (signer != _s().backend) revert InvalidBackendSignature(); - } - - function _verifyLeaf(bytes32[] calldata path, bytes32 root, string memory prefix, bytes memory value) - internal - pure - { - if (path.length > 32) revert MerklePathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - if (!MerkleProof.verify(path, root, leaf)) revert InvalidMerkleProof(); - } - - /// @notice Verify the immutable-id recv leaf. - /// Snippet = idPrefix + id + idSuffix. The suffix anchors the id's - /// end (closing `"` for X, `,` for GitHub's bare number) so a - /// truncated number can't match a longer real id. An empty userId is - /// rejected here for fail-early parity with `_registerHandle` / - /// `_linkHandle` (and the ZK/OIDC verifiers), which also require an id. - function _verifyId(FullTlsProof calldata proof, PlatformConfig storage pc, string calldata userId) internal view { - if (bytes(userId).length == 0) revert UserIdRequired(); - require(bytes(pc.idPrefix).length != 0, "id not supported"); - if (proof.idPath.length > 32) revert MerklePathTooLong(); - bytes memory snippet = abi.encodePacked(pc.idPrefix, userId, pc.idSuffix); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", snippet)))); - if (!MerkleProof.verify(proof.idPath, proof.transcriptRoot, leaf)) revert InvalidMerkleProof(); - } - - /// @notice Verify the username Merkle leaf matches the expected JSON snippet. - /// handlePrefix already includes quotes, colon, and any whitespace (e.g. `"login":"` or `"email": "`). - function _verifyUsernameLeaf( - bytes32[] calldata path, - bytes32 root, - string memory handlePrefix, - string memory username - ) internal pure { - if (path.length > 32) revert MerklePathTooLong(); - bytes memory snippet = abi.encodePacked(handlePrefix, username, '"'); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", snippet)))); - if (!MerkleProof.verify(path, root, leaf)) revert InvalidMerkleProof(); - } - - function _authorizeUpgrade(address) internal override onlyOwner {} -} diff --git a/solidity/archived/login/SnapshotProxy.sol b/solidity/archived/login/SnapshotProxy.sol deleted file mode 100644 index 5be04da0..00000000 --- a/solidity/archived/login/SnapshotProxy.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title SnapshotProxy — minimal delegating proxy with snapshotted implementation. -/// @notice Stores the implementation address in the ERC1967 slot at deploy time. -/// NOT automatically upgraded when a beacon changes — each wallet keeps -/// the implementation it was deployed with. The implementation contract -/// can update the slot via UUPS-style self-upgrade (upgrade_to_latest). -contract SnapshotProxy { - /// @dev ERC1967 implementation storage slot. - /// bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1) - bytes32 private constant _IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - constructor(address implementation) { - assembly { - sstore(_IMPL_SLOT, implementation) - } - } - - fallback() external payable { - assembly { - let impl := sload(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) - calldatacopy(0, 0, calldatasize()) - let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } -} diff --git a/solidity/archived/login/WalletFactory.sol b/solidity/archived/login/WalletFactory.sol deleted file mode 100644 index 55102cd7..00000000 --- a/solidity/archived/login/WalletFactory.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import {SnapshotProxy} from "./SnapshotProxy.sol"; -import {WebWallet} from "./WebWallet.sol"; - -/// @title WalletFactory — Factory for WebWallet instances -/// @notice Deploys WebWallet instances behind SnapshotProxy using CREATE. -/// Each wallet snapshots the current beacon implementation at deploy time -/// and is NOT automatically upgraded when the beacon changes. -/// Users can optionally upgrade via WebWallet.upgrade_to_latest(). -/// -/// @dev The beacon tracks the latest WebWallet implementation. New deployments -/// read beacon.implementation() and freeze that address in the proxy. -/// Upgrading the beacon only affects future deployments (and wallets whose -/// users explicitly opt in via upgrade_to_latest). -contract WalletFactory is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { - UpgradeableBeacon public beacon; - address public registry; - - /// @notice True if `wallet` was deployed by this factory. - mapping(address => bool) public isRegisteredWallet; - - // ─── Events ───────────────────────────────────────────────────── - - event WalletCreated(address indexed wallet, bytes32 indexed firstIdentity); - event WalletImplementationUpgraded(address indexed newImplementation); - - // ─── Errors ───────────────────────────────────────────────────── - - error OnlyRegistry(); - - // ─── Initializer ──────────────────────────────────────────────── - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @param owner_ Factory owner (the governance multisig). - /// @param walletImpl_ Initial WebWallet implementation address. - /// @param registry_ Registry contract address. - function initialize(address owner_, address walletImpl_, address registry_) external initializer { - __Ownable_init(owner_); - __Ownable2Step_init(); - __UUPSUpgradeable_init(); - beacon = new UpgradeableBeacon(walletImpl_, address(this)); - registry = registry_; - } - - // ─── Wallet deployment ────────────────────────────────────────── - - /// @notice Deploy a new WebWallet for a user's first social identity. - /// Uses CREATE opcode — address is non-deterministic. - /// The wallet's implementation is snapshotted from the beacon at deploy time. - /// Only callable by Registry. - /// - /// @param firstPlatform Platform domain of the user's first identity. - /// @param firstHandle Handle/username of the user's first identity (display). - /// @param firstUserId Immutable platform id of the user's first identity. - /// @return wallet Address of the deployed SnapshotProxy. - function deploy_web_wallet(string calldata firstPlatform, string calldata firstHandle, string calldata firstUserId) - external - returns (address wallet) - { - if (msg.sender != registry) revert OnlyRegistry(); - - // Snapshot current implementation from beacon. - address impl = beacon.implementation(); - - // Deploy SnapshotProxy with CREATE (non-deterministic address). - SnapshotProxy proxy = new SnapshotProxy(impl); - wallet = address(proxy); - - // Initialize the wallet through the proxy. - WebWallet(payable(wallet)).initialize(registry, address(this), firstPlatform, firstHandle); - - // Key the identity by the immutable id (matches WebWallet session keys), so - // a later handle rename/recycle can't make indexers mis-associate the wallet. - bytes32 firstIdentity = keccak256(abi.encode(firstPlatform, firstUserId)); - isRegisteredWallet[wallet] = true; - emit WalletCreated(wallet, firstIdentity); - } - - // ─── Latest implementation ───────────────────────────────────── - - /// @notice Returns the current WebWallet implementation from the beacon. - /// Used by WebWallet.upgradeToLatest() for opt-in upgrades. - function latestImplementation() external view returns (address) { - return beacon.implementation(); - } - - // ─── Beacon upgrade ───────────────────────────────────────────── - - /// @notice Upgrade the WebWallet implementation in the beacon. - /// Only affects future deployments and wallets that opt in. - function upgradeWalletImplementation(address newImplementation) external onlyOwner { - beacon.upgradeTo(newImplementation); - emit WalletImplementationUpgraded(newImplementation); - } - - function setRegistry(address newRegistry) external onlyOwner { - registry = newRegistry; - } - - // ─── Internal ─────────────────────────────────────────────────── - - function _authorizeUpgrade(address) internal override onlyOwner {} -} diff --git a/solidity/archived/login/WebWallet.sol b/solidity/archived/login/WebWallet.sol deleted file mode 100644 index 8ed994c2..00000000 --- a/solidity/archived/login/WebWallet.sol +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -interface IWalletFactory { - function latestImplementation() external view returns (address); -} - -/// @title WebWallet — Per-user social identity wallet (SnapshotProxy instance) -/// @notice One instance is deployed per user via WalletFactory. -/// Groups the user's social platform identities and configures -/// the multisig threshold. -/// -/// @dev General multisig executor: any on-chain action (transfers, withdrawals, -/// identity linking, etc.) goes through execute(). The wallet verifies that -/// enough distinct (platform, userId) identities have signed, then forwards -/// the call to the target contract. -/// -/// Session keys are stored per (platform, userId) identity as addresses. -/// The identity key is keccak256(abi.encode(platform, userId)) — keyed by the -/// immutable platform id, NOT the mutable handle, so a rename never moves a -/// session (revoke/threshold stay stable). Registry is the sole caller of -/// register_session(). -contract WebWallet is Initializable, UUPSUpgradeable { - address public registry; - address public factory; - - /// @notice M-of-N approval threshold for multisig. - /// Threshold counts distinct (platform, userId) identities, not distinct keys. - uint8 public threshold; - - /// @notice Replay-protection nonce for execute(). - uint256 public nonce; - - // ─── Identity tracking ────────────────────────────────────────── - - /// @dev Set of active identity keys (platformHandleKey). An identity is active - /// when it has at least one session in _sessionList. - bytes32[] private _identityKeys; - mapping(bytes32 => uint256) private _identityKeyIndex; // key → index in _identityKeys (valid when key is in array) - - // ─── Session storage (address-based) ──────────────────────────── - - /// @notice Maps session address → keccak256(abi.encode(platform, userId)). - /// Zero means not a session. (Field name kept for storage-layout - /// compatibility across the UUPS upgrade; the value is now the id key.) - mapping(address => bytes32) private _sessionPlatformHandle; - - /// @notice identityKey → array of session addresses. - mapping(bytes32 => address[]) private _sessionList; - - /// @notice identityKey → sessionAddr → index+1 in _sessionList (0 = not present). - mapping(bytes32 => mapping(address => uint256)) private _sessionIndex; - - // ─── Events ───────────────────────────────────────────────────── - - event SessionRegistered(string platform, string handle, address indexed sessionAddr); - event SessionRevoked(string platform, string handle, address indexed sessionAddr); - event Executed(address indexed target, uint256 value, uint256 nonce); - event NativeReceived(address indexed from, uint256 amount); - event NativeSent(address indexed to, uint256 amount); - - // ─── Errors ───────────────────────────────────────────────────── - - error InvalidSignature(); - error ThresholdNotMet(); - error OnlyRegistry(); - error OnlySelf(); - error ExecutionFailed(bytes returnData); - error SessionNotFound(); - error ThresholdExceedsIdentityCount(); - error SessionAlreadyRegistered(); - - // ─── Initializer ──────────────────────────────────────────────── - - /// @notice Initialize this wallet. Called by WalletFactory after proxy deployment. - /// @param _registry Registry contract address (sole caller of register_session). - /// @param _factory WalletFactory address (used for opt-in upgrades). - /// @param firstPlatform Platform domain of the user's first identity. - /// @param firstHandle Handle/username of the user's first identity. - function initialize(address _registry, address _factory, string calldata firstPlatform, string calldata firstHandle) - external - initializer - { - registry = _registry; - factory = _factory; - threshold = 1; - // Identity will be added to _identityKeys when the first session is registered - // via register_session (which checks _sessionList[key].length == 0). - } - - receive() external payable { - emit NativeReceived(msg.sender, msg.value); - } - - // ─── Session registration (Registry only) ─────────────────────── - - /// @notice Register a session address for a (platform, userId) identity. Only callable by Registry. - /// @param platform Platform domain (e.g. "x.com"). - /// @param handle Current handle — emitted for display only; NOT part of the key. - /// @param userId Immutable platform id — the identity key is keccak(platform, userId). - /// @param sessionAddr The session address derived from ecrecover. - function register_session( - string calldata platform, - string calldata handle, - string calldata userId, - address sessionAddr - ) external { - if (msg.sender != registry) revert OnlyRegistry(); - require(sessionAddr != address(0), "zero session"); - require(bytes(userId).length != 0, "empty userId"); - - // Prevent duplicate session registration - if (_sessionPlatformHandle[sessionAddr] != bytes32(0)) revert SessionAlreadyRegistered(); - - _addSession(_identityKey(platform, userId), sessionAddr); - - emit SessionRegistered(platform, handle, sessionAddr); - } - - // ─── Session revocation (self-call via execute) ───────────────── - - /// @notice Revoke a session. Only callable by this contract (via execute with multisig). - /// @param platform Platform domain. - /// @param handle Current handle — emitted for display only; NOT part of the key. - /// @param userId Immutable platform id — the identity key is keccak(platform, userId). - /// @param sessionAddr The session address to revoke. - function revokeSession( - string calldata platform, - string calldata handle, - string calldata userId, - address sessionAddr - ) external { - if (msg.sender != address(this)) revert OnlySelf(); - - _removeSession(_identityKey(platform, userId), sessionAddr); - - emit SessionRevoked(platform, handle, sessionAddr); - } - - /// @notice One-time migration: move a session from the legacy (platform, handle) - /// key to the (platform, userId) key. Registry-gated, idempotent. Used to - /// re-key sessions that were registered before the id-keying upgrade. - /// @dev No-op when the session is already under the id key. Emits SessionRegistered - /// (unchanged signature) so indexers pick the session up under the id key. - function reregisterSessionById( - string calldata platform, - string calldata handle, - string calldata userId, - address sessionAddr - ) external { - if (msg.sender != registry) revert OnlyRegistry(); - require(bytes(userId).length != 0, "empty userId"); - - bytes32 newKey = _identityKey(platform, userId); - bytes32 current = _sessionPlatformHandle[sessionAddr]; - if (current == newKey) return; // already id-keyed - bytes32 oldKey = keccak256(abi.encode(platform, handle)); - if (current != oldKey || _sessionIndex[oldKey][sessionAddr] == 0) revert SessionNotFound(); - - _removeSession(oldKey, sessionAddr); // clears _sessionPlatformHandle[sessionAddr] - _addSession(newKey, sessionAddr); - - emit SessionRegistered(platform, handle, sessionAddr); - } - - // ─── Session storage helpers ──────────────────────────────────── - - function _identityKey(string calldata platform, string calldata userId) internal pure returns (bytes32) { - return keccak256(abi.encode(platform, userId)); - } - - /// @dev Add `sessionAddr` under `key`, tracking the identity on first session. - function _addSession(bytes32 key, address sessionAddr) internal { - if (_sessionList[key].length == 0) { - _identityKeyIndex[key] = _identityKeys.length; - _identityKeys.push(key); - } - _sessionPlatformHandle[sessionAddr] = key; - _sessionList[key].push(sessionAddr); - _sessionIndex[key][sessionAddr] = _sessionList[key].length; // 1-indexed - } - - /// @dev Remove `sessionAddr` from `key` (swap-pop), untracking the identity on - /// its last session. Reverts SessionNotFound if not present under `key`. - function _removeSession(bytes32 key, address sessionAddr) internal { - uint256 oneIndexed = _sessionIndex[key][sessionAddr]; - if (oneIndexed == 0) revert SessionNotFound(); - - uint256 idx = oneIndexed - 1; - uint256 lastIdx = _sessionList[key].length - 1; - - if (idx != lastIdx) { - address lastAddr = _sessionList[key][lastIdx]; - _sessionList[key][idx] = lastAddr; - _sessionIndex[key][lastAddr] = oneIndexed; - } - - _sessionList[key].pop(); - delete _sessionIndex[key][sessionAddr]; - delete _sessionPlatformHandle[sessionAddr]; - - // If last session for this identity was removed, clean up identity tracking - if (_sessionList[key].length == 0) { - uint256 keyIdx = _identityKeyIndex[key]; - uint256 lastKeyIdx = _identityKeys.length - 1; - if (keyIdx != lastKeyIdx) { - bytes32 lastKey = _identityKeys[lastKeyIdx]; - _identityKeys[keyIdx] = lastKey; - _identityKeyIndex[lastKey] = keyIdx; - } - _identityKeys.pop(); - delete _identityKeyIndex[key]; - } - } - - // ─── General executor ─────────────────────────────────────────── - - /// @notice Execute an arbitrary call after verifying multisig threshold. - /// @param target Contract to call. - /// @param value ETH value to send. - /// @param data Calldata for the target. - /// @param sigs Signatures over the execute digest. Signer addresses are - /// recovered via ecrecover — no need to pass them separately. - /// @return result Return data from the call. - function execute(address target, uint256 value, bytes calldata data, bytes[] calldata sigs) - external - payable - returns (bytes memory result) - { - uint256 currentNonce = nonce; - bytes32 digest = - keccak256(abi.encode(target, value, keccak256(data), currentNonce, block.chainid, address(this))); - - _requireMultiSig(sigs, digest); - - nonce = currentNonce + 1; - - bool success; - (success, result) = target.call{value: value}(data); - if (!success) revert ExecutionFailed(result); - - emit Executed(target, value, currentNonce); - if (value > 0) emit NativeSent(target, value); - } - - // ─── Batch executor ─────────────────────────────────────────────── - - /// @notice Execute multiple calls atomically after verifying multisig threshold once. - /// @param targets Array of contracts to call. - /// @param values Array of ETH values to send per call. - /// @param datas Array of calldata per call. - /// @param sigs Signatures over the batch digest. - /// @return results Array of return data from each call. - function executeBatch( - address[] calldata targets, - uint256[] calldata values, - bytes[] calldata datas, - bytes[] calldata sigs - ) external payable returns (bytes[] memory results) { - require(targets.length == values.length && values.length == datas.length, "length mismatch"); - - uint256 currentNonce = nonce; - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(this))); - - _requireMultiSig(sigs, digest); - - nonce = currentNonce + 1; - - results = new bytes[](targets.length); - for (uint256 i = 0; i < targets.length; i++) { - bool success; - (success, results[i]) = targets[i].call{value: values[i]}(datas[i]); - if (!success) revert ExecutionFailed(results[i]); - if (values[i] > 0) emit NativeSent(targets[i], values[i]); - } - - emit Executed(address(0), 0, currentNonce); - } - - // ─── Upgrade ───────────────────────────────────────────────────── - - /// @notice Upgrade this wallet to the latest implementation from the factory. - /// Must be called via execute() (self-call with multisig). - function upgradeToLatest() external { - require(msg.sender == address(this), "only via execute()"); - address newImpl = IWalletFactory(factory).latestImplementation(); - upgradeToAndCall(newImpl, ""); - } - - /// @dev Authorize UUPS upgrades — only via self-call (execute with multisig). - function _authorizeUpgrade(address) internal override { - require(msg.sender == address(this), "only via execute()"); - } - - // ─── Self-callable setters (called via execute → self-call) ──── - - /// @notice Set the multisig threshold. Only callable by this contract (via execute). - function setThreshold(uint8 newThreshold) external { - if (msg.sender != address(this)) revert OnlySelf(); - require(newThreshold > 0, "zero threshold"); - if (newThreshold > _identityKeys.length) revert ThresholdExceedsIdentityCount(); - threshold = newThreshold; - } - - // ─── Views ────────────────────────────────────────────────────── - - /// @notice Check if an identity hash has at least one active session. - function isIdentity(bytes32 identityHash) external view returns (bool) { - return _sessionList[identityHash].length > 0; - } - - function identityCount() external view returns (uint256) { - return _identityKeys.length; - } - - function identityAt(uint256 index) external view returns (bytes32) { - return _identityKeys[index]; - } - - function sessionCount(bytes32 platformHandleKey) external view returns (uint256) { - return _sessionList[platformHandleKey].length; - } - - function sessionAt(bytes32 platformHandleKey, uint256 index) external view returns (address) { - return _sessionList[platformHandleKey][index]; - } - - function isSession(address addr) external view returns (bool) { - return _sessionPlatformHandle[addr] != bytes32(0); - } - - function sessionIdentity(address addr) external view returns (bytes32) { - return _sessionPlatformHandle[addr]; - } - - // ─── Internal helpers ─────────────────────────────────────────── - - /// @dev Verify that >= threshold distinct (platform, userId) identities have signed - /// digest. Recovers signer addresses from signatures via ecrecover. - function _requireMultiSig(bytes[] calldata sigs, bytes32 digest) internal view { - uint256 len = sigs.length; - bytes32[] memory seenPairs = new bytes32[](len); - uint256 distinctCount = 0; - - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - - for (uint256 i = 0; i < len; i++) { - address signer = _recoverSigner(sigs[i], ethHash); - - bytes32 key = _sessionPlatformHandle[signer]; - if (key == bytes32(0)) continue; // not an active session — skip - - // Deduplicate by (platform, handle) key - bool isDup = false; - for (uint256 j = 0; j < distinctCount; j++) { - if (seenPairs[j] == key) { - isDup = true; - break; - } - } - if (isDup) continue; - - seenPairs[distinctCount++] = key; - } - - if (distinctCount < threshold) revert ThresholdNotMet(); - } - - /// @dev Recover signer address from a 65-byte signature. - function _recoverSigner(bytes calldata sig, bytes32 ethHash) internal pure returns (address) { - require(sig.length == 65, "bad sig"); - - uint8 v = uint8(sig[64]); - bytes32 r; - bytes32 s; - assembly { - r := calldataload(sig.offset) - s := calldataload(add(sig.offset, 32)) - } - - address recovered = ecrecover(ethHash, v, r, s); - if (recovered == address(0)) revert InvalidSignature(); - return recovered; - } -} diff --git a/solidity/archived/login/oidc/GoogleOidcVerifier.sol b/solidity/archived/login/oidc/GoogleOidcVerifier.sol deleted file mode 100644 index b39f4f81..00000000 --- a/solidity/archived/login/oidc/GoogleOidcVerifier.sol +++ /dev/null @@ -1,793 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -import {IOidcVerifier} from "./IOidcVerifier.sol"; -import {IVerifier} from "./Verifier.sol"; -import {INotary} from "../../notary/INotary.sol"; - -/// IOidcVerifier implementation for Google OIDC. -/// -/// Owns three things: -/// -/// 1. A pointer to the shared Notary contract. Used to verify TLS-notary -/// attestations over `googleapis.com/oauth2/v3/certs` that rotate the -/// RSA JWKS. The rotation digest deliberately names no chain and no -/// contract, so the same notarized reading is valid on every deployment -/// that trusts the notary — that replay is a feature; only WHO the -/// notary is lives in the Notary contract. -/// 2. The kid → modulusHash mapping populated by `rotateRoots`. Each entry -/// has an expiry; old keys age out automatically. -/// 3. A reference to the UltraHonk Verifier deployed for the `jwt_email` -/// Noir circuit. `verifyAndExtract` checks the user's ZK proof and -/// ensures its `modulus` public input lines up with a currently-trusted -/// kid. -/// 4. The accepted Google OAuth client id (`expectedAudienceHash`). The -/// `jwt_email` circuit is application-agnostic — it binds whatever `aud` -/// the token carries and exposes its SHA-256 — so this contract is what -/// pins registrations to one app. Configurable at deploy and by the owner. -/// -/// The rotation trust model, in one line: the notary attests the reading, time -/// does the expiry, and nobody owns rotation. `rotate`/`rotateRoots`/`prune` -/// are open to any caller; every trusted key carries a stamp checked at -/// use-site, so an expired key stops being trusted with no transaction from -/// anyone. The owner's only lever over the key list is `untrustModulus`, which -/// can only REMOVE trust. -contract GoogleOidcVerifier is IOidcVerifier, Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { - // ─── Constants ──────────────────────────────────────────────── - bytes32 private constant EXPECTED_DOMAIN_HASH = keccak256(bytes("www.googleapis.com")); - bytes private constant EXPECTED_DOMAIN = bytes("www.googleapis.com"); - bytes private constant EXPECTED_ENDPOINT = bytes("/oauth2/v3/certs"); - uint256 public constant FRESHNESS_WINDOW = 1 hours; - uint256 public constant CLOCK_SKEW_GRACE = 5 minutes; - uint256 public constant DEFAULT_MODULUS_TTL = 30 days; - /// The most distinct kids the list will track at once. Google publishes a - /// handful of overlapping keys; a notarized reading cannot invent kids, so - /// this cap is headroom, not a working limit. It exists so the enumeration - /// stays a bounded loop no submitter — honest or not — can bloat. - uint256 public constant MAX_TRACKED_KIDS = 16; - /// How much trusted runway `needsRotation()` insists on. Rotation re-stamps - /// every key for DEFAULT_MODULUS_TTL, so a keeper acting on this signal - /// rotates roughly weekly rather than racing the expiry. - uint256 public constant RENEWAL_MARGIN = 7 days; - uint256 public constant TOTAL_PUBLIC_INPUTS = 28; - /// Max `aud` (client id) length the jwt_email circuit hashes — must match its - /// `AUDIENCE_MAX`. A configured client id longer than this can never match. - uint256 public constant MAX_AUDIENCE_BYTES = 128; - - // ─── Storage ────────────────────────────────────────────────── - // Owner lives in Ownable2StepUpgradeable's namespaced storage (owner()). - address public registry; - /// UltraHonk verifier for the jwt_email circuit. Set in `initialize` - /// (not immutable — this contract sits behind a UUPS proxy). - IVerifier public verifier; - - /// The Notary contract a rotation's attestation is checked with. - INotary public notaryContract; - /// kid keccak → modulusHash (the value stored is the limb-keccak the - /// circuit produces). - mapping(bytes32 => bytes32) public modulusOfKid; - mapping(bytes32 => uint256) public expiresAtKid; - /// modulusHash → expiry. Lets `verifyAndExtract` check trust without - /// needing kid (the JWT Noir circuit doesn't expose kid). - mapping(bytes32 => uint256) public trustedHashExpiresAt; - - /// SHA-256 of the Google OAuth client id (the JWT `aud`) this verifier - /// accepts. Configurable per deployment: set at `initialize` and mutable by - /// the owner (`setExpectedAudience`). The circuit binds `aud` to the token - /// and exposes SHA-256(aud) as public inputs [24..26); `_verifyAndExtract` - /// checks those halves against this value so a token minted for a DIFFERENT - /// Google OAuth app (confused deputy) cannot register here. - bytes32 public expectedAudienceHash; - - /// Timestamp of the rotation proof that last wrote each kid. Rotation is - /// permissionless, so ANY caller may replay a still-fresh proof; without - /// this, replaying an older proof inside FRESHNESS_WINDOW silently rolls a - /// kid back to a modulus Google has already retired and re-stamps it for - /// another DEFAULT_MODULUS_TTL. Monotonic rather than a one-shot nullifier: - /// a nullifier would let a front-runner consume the digest and brick the - /// honest keeper, which is exactly what the open caller set makes cheap. - mapping(bytes32 => uint256) public rotatedAtKid; - - /// Every kid currently tracked, so keepers can enumerate the list without - /// an indexer. Bounded by MAX_TRACKED_KIDS; expired entries leave via - /// `prune()`, which anyone may call. - bytes32[] private _trackedKids; - /// kid keccak → index+1 in `_trackedKids` (0 = not tracked). - mapping(bytes32 => uint256) private _trackedKidIndex; - /// The timestamp of the freshest reading ever applied — the notary's - /// observation time, not the block it landed in. A keeper compares this to - /// wall-clock to decide whether its reading would be news. - uint256 public freshestObservedAt; - - /// Reserved storage slots for future upgrades (UUPS). New state must be - /// appended before this gap and the gap shrunk to preserve layout. - uint256[47] private __gap; - - // ─── Events / errors ────────────────────────────────────────── - event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); - /// A key stopped being trusted before its stamp ran out — either a rotation - /// replaced the modulus a kid carried, or the owner untrusted it outright. - event ModulusUntrusted(bytes32 indexed modulusHash); - /// One key written by a rotation, with the reading's own timestamp. What a - /// keeper watches: `observedAt` orders readings, `expiresAt` says when this - /// entry stops being trusted if no rotation lands before then. - event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - /// An expired key left the tracked set. Permissionless — time retired the - /// key, `prune()` merely reclaimed the slot. - event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); - /// Emitted whenever the accepted Google client id changes. `clientId` is the - /// plaintext id when configured via the string setter, or "" when set by raw - /// hash; `audienceHash` is authoritative. - event AudienceConfigured(bytes32 indexed audienceHash, string clientId); - - error ZeroAddress(); - error TooManyKids(); - error UnknownNotary(); - error WrongDomain(); - error FutureProof(); - error StaleProof(); - error MerkleMismatch(); - error PathTooLong(); - error WrongChain(); - error WrongRegistry(); - error MissingKidInLeaf(); - error MissingNInLeaf(); - error InvalidModulusLength(); - error InvalidB64Char(); - error InvalidB64Length(); - - error WrongPublicInputCount(); - error BadHonkProof(); - error WrongEmail(); - error WrongAddress(); - error WrongSub(); - error EmptySub(); - error UntrustedModulus(); - error JwtExpired(); - /// Proof's `aud` (SHA-256) doesn't match the configured client id, or no - /// client id has been configured yet. - error WrongAudience(); - /// A client id / audience hash of zero was supplied. - error EmptyAudience(); - /// A client id longer than `MAX_AUDIENCE_BYTES` was supplied — the circuit - /// couldn't produce a matching hash, so it's rejected at config time. - error AudienceTooLong(); - - // ─── Types ──────────────────────────────────────────────────── - struct NotarizedJwksProof { - bytes notarySignature; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] domainPath; - bytes32[] endpointPath; - } - - struct JwkClaim { - bytes jwkBytes; - bytes32[] jwkPath; - bytes kid; - bytes nB64url; - } - - /// User-side: bundles everything needed to verify a JWT Honk proof. - struct UserProof { - bytes honkProof; - bytes32[] publicInputs; - string email; - address sessionKey; - /// Immutable Google account id (JWT `sub`). Validated against the - /// proof's packed `sub` public input; identity binds on this. - string sub; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// Initialize the proxy. Mirrors XZkVerifier's UUPS pattern: the Honk - /// `verifier`, owner, and the Notary pointer are set here (not in the - /// constructor) because this contract sits behind an ERC1967 proxy. - /// @param notaryContract_ The shared Notary contract (INotary). Notary key - /// rotation happens THERE; this contract holds only the pointer. - /// @param clientId The Google OAuth client id whose id_tokens this verifier - /// accepts (the JWT `aud`). Bound into the proof by the circuit and - /// enforced on chain — see `expectedAudienceHash`. Changeable later - /// by the owner via `setExpectedAudience`. - function initialize(IVerifier _verifier, address _owner, address notaryContract_, string calldata clientId) - external - initializer - { - if (address(_verifier) == address(0) || _owner == address(0) || notaryContract_ == address(0)) { - revert ZeroAddress(); - } - __Ownable_init(_owner); - __UUPSUpgradeable_init(); - verifier = _verifier; - _setExpectedAudience(clientId); - notaryContract = INotary(notaryContract_); - } - - /// The current notary signer, read through the Notary contract. - function notary() external view returns (address) { - return notaryContract.notary(); - } - - /// Only the owner may upgrade the implementation. - function _authorizeUpgrade(address) internal override onlyOwner {} - - /// @dev Renouncing ownership is disabled: a zero owner would permanently - /// brick the UUPS upgrade path and every admin function. - function renounceOwnership() public pure override { - revert("renounce disabled"); - } - - // ─── Admin ──────────────────────────────────────────────────── - - /// Record which Registry fronts this verifier. Informational only — - /// `rotateRoots`/`rotate` are permissionless and nothing here gates on this - /// address; it survives from the era when rotation had a caller set, and - /// stays because the storage slot and ABI already shipped. - function setRegistry(address r) external onlyOwner { - registry = r; - } - - /// Stop trusting a key before anything replaces it. - /// - /// Rotation already retires the key a kid used to carry, which covers the - /// ordinary case. This covers the one that cannot wait: a key Google has - /// not rotated yet, or has rotated in a way this list has not seen. - /// Owner-only, and it can only ever REMOVE trust: the worst an owner does - /// with it is refuse logins, which the rotation any keeper can submit - /// undoes. - function untrustModulus(bytes32 modulusHash) external onlyOwner { - delete trustedHashExpiresAt[modulusHash]; - emit ModulusUntrusted(modulusHash); - } - - /// Reconfigure the accepted Google OAuth client id (the JWT `aud`). Use this - /// on upgrade or when rotating which app may register through this verifier. - /// The circuit is application-agnostic, so no redeploy of the Honk verifier - /// is needed — only this on-chain expectation changes. - function setExpectedAudience(string calldata clientId) external onlyOwner { - _setExpectedAudience(clientId); - } - - /// Lower-level variant: set the expected `aud` hash directly (SHA-256 of the - /// client id). Handy when the hash is known but the plaintext id isn't, and - /// used by tests. Prefer `setExpectedAudience` for the readable audit trail. - function setExpectedAudienceHash(bytes32 audienceHash) external onlyOwner { - if (audienceHash == bytes32(0)) revert EmptyAudience(); - expectedAudienceHash = audienceHash; - emit AudienceConfigured(audienceHash, ""); - } - - function _setExpectedAudience(string memory clientId) internal { - uint256 len = bytes(clientId).length; - if (len == 0) revert EmptyAudience(); - // The circuit hashes at most AUDIENCE_MAX (128) bytes of `aud`. A client - // id longer than that could never produce a matching `audience_hash`, so - // configuring it would silently make every registration revert - // WrongAudience. Reject it here (real Google client ids are ~72 bytes). - if (len > MAX_AUDIENCE_BYTES) revert AudienceTooLong(); - bytes32 audienceHash = sha256(bytes(clientId)); - expectedAudienceHash = audienceHash; - emit AudienceConfigured(audienceHash, clientId); - } - - // ─── IPlatformProver ────────────────────────────────────────── - function platformName() external pure override returns (string memory) { - return "google"; - } - - function rotateRoots(bytes calldata rotationProof) external override { - (NotarizedJwksProof memory p, JwkClaim[] memory claims) = - abi.decode(rotationProof, (NotarizedJwksProof, JwkClaim[])); - _rotate(p, claims); - } - - function verifyAndExtract(bytes calldata proof) - external - view - override - returns (string memory handle, address sessionKey, uint256 expiresAt, string memory userId) - { - UserProof memory u = abi.decode(proof, (UserProof)); - return _verifyAndExtract(u); - } - - // ─── Direct entry points (typed; useful for tests / scripts) ── - /// Apply a notarized reading of Google's JWKS. Anyone may call it — the - /// proof is the whole authorization: a keeper needs gas and nothing else. - function rotate(NotarizedJwksProof calldata proof, JwkClaim[] calldata claims) external { - _rotate(proof, claims); - } - - /// Drop every expired kid from the tracked set. Anyone may call it. - /// - /// Expiry itself needs no transaction — `verifyAndExtract` checks the stamp - /// at use-site, so an expired key is already untrusted the second its TTL - /// passes. Pruning only reclaims enumeration slots (and the storage - /// refund). Rotation calls this by itself when the set is full, so even the - /// prune is optional housekeeping. - function prune() external { - _pruneExpired(); - } - - // ─── Keeper views ───────────────────────────────────────────── - - /// One tracked key, as a keeper sees it. - struct RootInfo { - bytes32 kidHash; - bytes32 modulusHash; - /// The notarized reading that last wrote this kid (proof timestamp). - uint256 observedAt; - /// When this entry stops being trusted, absent a fresher rotation. - uint256 expiresAt; - } - - /// Every tracked key with its provenance and expiry, expired ones included - /// until someone prunes. One call tells a keeper the whole state of the - /// list. - function currentRoots() external view returns (RootInfo[] memory infos) { - uint256 n = _trackedKids.length; - infos = new RootInfo[](n); - for (uint256 i = 0; i < n; i++) { - bytes32 kidHash = _trackedKids[i]; - infos[i] = RootInfo({ - kidHash: kidHash, - modulusHash: modulusOfKid[kidHash], - observedAt: rotatedAtKid[kidHash], - expiresAt: expiresAtKid[kidHash] - }); - } - } - - /// True when no key is guaranteed trusted RENEWAL_MARGIN from now — the - /// single bit a keeper polls to decide whether to fetch a fresh reading. - /// True from deployment until the first rotation lands. - function needsRotation() external view returns (bool) { - uint256 horizon = block.timestamp + RENEWAL_MARGIN; - uint256 n = _trackedKids.length; - for (uint256 i = 0; i < n; i++) { - bytes32 modulusHash = modulusOfKid[_trackedKids[i]]; - if (trustedHashExpiresAt[modulusHash] > horizon) return false; - } - return true; - } - - function verifyDirect(UserProof calldata u) - external - view - returns (string memory handle, address sessionKey, uint256 expiresAt, string memory userId) - { - return _verifyAndExtract(u); - } - - // ─── Internal: rotation ─────────────────────────────────────── - function _rotate(NotarizedJwksProof memory p, JwkClaim[] memory claims) internal { - // The digest deliberately names no chain and no contract (see the - // contract comment); the Notary contract checks the attestation over - // it — EIP-191 + recover + signer compare today. - bytes32 digest = _notaryDigest(p); - if (!notaryContract.verify(digest, p.notarySignature)) revert UnknownNotary(); - - if (p.timestamp > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); - if (block.timestamp > p.timestamp + FRESHNESS_WINDOW) revert StaleProof(); - - // PERMISSIONLESS: anyone may submit a rotation. There is deliberately no - // one-shot nullifier here — with an open caller set it would BE the - // attack: a front-runner could consume a proof's digest (optionally with - // a claims subset) and brick the honest keeper's rotation. Freshness is - // enforced by the notary signature plus the FRESHNESS_WINDOW / - // CLOCK_SKEW_GRACE bounds above; re-applying the same rotation is - // idempotent. - - if (p.domainHash != EXPECTED_DOMAIN_HASH) revert WrongDomain(); - _verifyLeaf(p.domainPath, p.transcriptRoot, "domain:", EXPECTED_DOMAIN); - _verifyLeaf(p.endpointPath, p.transcriptRoot, "endpoint:", EXPECTED_ENDPOINT); - - // Monotonic high-water mark of reading timestamps. Replaying an older - // proof leaves it alone, so it only ever moves forward. - if (p.timestamp > freshestObservedAt) freshestObservedAt = p.timestamp; - - uint256 expiry = block.timestamp + DEFAULT_MODULUS_TTL; - for (uint256 i = 0; i < claims.length; i++) { - _processClaim(claims[i], p.transcriptRoot, expiry, p.timestamp); - } - } - - function _notaryDigest(NotarizedJwksProof memory p) internal pure returns (bytes32) { - return keccak256( - abi.encode( - p.domainHash, - p.clientRandom, - p.serverRandom, - keccak256(p.serverEphemeralKey), - p.transcriptRoot, - p.timestamp - ) - ); - } - - function _processClaim(JwkClaim memory c, bytes32 transcriptRoot, uint256 expiry, uint256 provenAt) internal { - if (c.jwkPath.length > 32) revert PathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", c.jwkBytes)))); - if (!_merkleVerifyMem(c.jwkPath, transcriptRoot, leaf)) revert MerkleMismatch(); - - if (!_containsKeyValue(c.jwkBytes, "kid", c.kid)) revert MissingKidInLeaf(); - if (!_containsKeyValue(c.jwkBytes, "n", c.nB64url)) revert MissingNInLeaf(); - - bytes memory rawN = _b64urlDecode(c.nB64url); - if (rawN.length != 256) revert InvalidModulusLength(); - bytes32[18] memory limbs = _splitLimbs(rawN); - - bytes memory packed = new bytes(18 * 32); - for (uint256 i = 0; i < 18; i++) { - bytes32 v = limbs[i]; - assembly { - mstore(add(add(packed, 32), mul(i, 32)), v) - } - } - bytes32 modulusHash = keccak256(packed); - - bytes32 kidHash = keccak256(c.kid); - // Ignore a claim proved no later than the one already applied — the - // monotonic rule that makes replay harmless. Rotation is open and a - // proof binds no contract, so anyone can replay any still-fresh - // reading anywhere, forever; ignoring (never reverting) keeps a - // front-runner from bricking the honest keeper's batch by landing one - // claim from it first, and skipping (never applying) keeps an older - // reading from rolling a kid back or re-stamping its TTL. - if (provenAt < rotatedAtKid[kidHash]) return; - bytes32 previous = modulusOfKid[kidHash]; - // A byte-identical resubmission of the applied reading is a no-op: - // nothing is written, nothing is emitted, and in particular the TTL is - // NOT re-stamped — so spamming the same proof neither grows state nor - // stretches trust. - if (provenAt == rotatedAtKid[kidHash] && previous == modulusHash) return; - rotatedAtKid[kidHash] = provenAt; - - // The key this kid used to carry stops being trusted NOW, rather than - // when its thirty-day stamp runs out. `_verifyAndExtract` resolves by - // modulus, not by kid, so leaving the old entry would keep a retired - // key usable for the rest of its TTL — exactly the window a - // compromised key would be used in. Google publishes overlapping keys - // and tokens live about an hour, so the cost is only that a token - // signed by the key Google just retired stops verifying a little - // early; the user signs in again. - if (previous != bytes32(0) && previous != modulusHash) { - delete trustedHashExpiresAt[previous]; - emit ModulusUntrusted(previous); - } else if (previous == bytes32(0)) { - _trackKid(kidHash); - } - - modulusOfKid[kidHash] = modulusHash; - expiresAtKid[kidHash] = expiry; - trustedHashExpiresAt[modulusHash] = expiry; - emit ModulusRotated(kidHash, string(c.kid), modulusHash, expiry); - emit RootApplied(kidHash, modulusHash, provenAt, expiry); - } - - /// Add a kid to the enumeration. Growth is doubly bounded: a kid can only - /// enter through a notarized reading of Google's own JWKS (a submitter - /// cannot invent one), and the set is capped — when full, expired entries - /// are pruned to make room, and only a set full of LIVE keys refuses. - function _trackKid(bytes32 kidHash) internal { - if (_trackedKidIndex[kidHash] != 0) return; - if (_trackedKids.length >= MAX_TRACKED_KIDS) { - _pruneExpired(); - if (_trackedKids.length >= MAX_TRACKED_KIDS) revert TooManyKids(); - } - _trackedKids.push(kidHash); - _trackedKidIndex[kidHash] = _trackedKids.length; - } - - /// Swap-remove every expired kid. `rotatedAtKid` survives on purpose: it is - /// the monotonic floor that keeps a replayed old reading from resurrecting - /// the entry the prune just removed. - function _pruneExpired() internal { - uint256 i = _trackedKids.length; - while (i > 0) { - i--; - bytes32 kidHash = _trackedKids[i]; - if (expiresAtKid[kidHash] > block.timestamp) continue; - - bytes32 modulusHash = modulusOfKid[kidHash]; - delete modulusOfKid[kidHash]; - delete expiresAtKid[kidHash]; - // Only clear the verifier-facing stamp if it is genuinely spent — - // it can sit above the kid's own expiry only if some fresher - // rotation re-trusted the same modulus under another kid. - if (trustedHashExpiresAt[modulusHash] <= block.timestamp) { - delete trustedHashExpiresAt[modulusHash]; - } - - uint256 last = _trackedKids.length - 1; - if (i != last) { - bytes32 moved = _trackedKids[last]; - _trackedKids[i] = moved; - _trackedKidIndex[moved] = i + 1; - } - _trackedKids.pop(); - delete _trackedKidIndex[kidHash]; - emit RootPruned(kidHash, modulusHash); - } - } - - // ─── Internal: user verification ────────────────────────────── - function _verifyAndExtract(UserProof memory u) - internal - view - returns (string memory handle, address sessionKey, uint256 expiresAt, string memory userId) - { - if (u.publicInputs.length != TOTAL_PUBLIC_INPUTS) revert WrongPublicInputCount(); - - // 1. Honk proof - if (!verifier.verify(u.honkProof, u.publicInputs)) revert BadHonkProof(); - - // Public-input layout (mirrors the circuit): - // [0..18) modulus limbs - // [18..20) email packed (62 bytes) - // [20..22) nonce packed (62 bytes) - // [22] sub packed (31 bytes, JWT `sub`) - // [23] exp - // [24..26) SHA-256(aud), two big-endian 16-byte halves - // [26] chain_id (== block.chainid) - // [27] registry_addr (== msg.sender, the calling Registry) - bytes memory packed; - { - packed = new bytes(18 * 32); - for (uint256 i = 0; i < 18; i++) { - bytes32 v = u.publicInputs[i]; - assembly { - mstore(add(add(packed, 32), mul(i, 32)), v) - } - } - } - bytes32 modulusHash = keccak256(packed); - uint256 modExpiry = trustedHashExpiresAt[modulusHash]; - if (modExpiry == 0 || block.timestamp >= modExpiry) revert UntrustedModulus(); - - // 3. exp - uint256 jwtExp = uint256(u.publicInputs[23]); - if (jwtExp + CLOCK_SKEW_GRACE <= block.timestamp) revert JwtExpired(); - - // 4. email matches packed - { - (bytes32 e0, bytes32 e1) = _pack62(_padTo62(bytes(u.email))); - if (u.publicInputs[18] != e0 || u.publicInputs[19] != e1) revert WrongEmail(); - } - - // 5. session address matches packed nonce - { - (bytes32 n0, bytes32 n1) = _pack62(_padTo62(_addressToAscii(u.sessionKey))); - if (u.publicInputs[20] != n0 || u.publicInputs[21] != n1) revert WrongAddress(); - } - - // 5b. sub (immutable account id) matches packed public input. - // Reject an empty sub up front (defense-in-depth: identity binds on it, - // and Registry would otherwise revert with an opaque UserIdRequired). - { - if (bytes(u.sub).length == 0) revert EmptySub(); - bytes32 s0 = _pack31(_padTo31(bytes(u.sub))); - if (u.publicInputs[22] != s0) revert WrongSub(); - } - - // 6. Deployment binding: the proof commits chain_id + Registry address, - // so a proof minted for one (chain, Registry) cannot be replayed against - // another deployment that shares this verification key. `msg.sender` is - // the Registry that dispatched `verifyAndExtract`. - if (uint256(u.publicInputs[26]) != block.chainid) revert WrongChain(); - if (uint256(u.publicInputs[27]) != uint256(uint160(msg.sender))) revert WrongRegistry(); - - // 7. Audience: the circuit binds the JWT `aud` to the token and exposes - // SHA-256(aud) as publicInputs[24..26) (two big-endian 16-byte halves). - // Enforce it equals the client id this verifier was configured for, so a - // Google-signed id_token minted for a DIFFERENT OAuth app can't be - // replayed here (confused deputy). Layout mirrors the circuit's split: - // high 16 bytes in [24], low 16 bytes in [25]. - bytes32 aud = expectedAudienceHash; - if (aud == bytes32(0)) revert WrongAudience(); // not configured - if ( - uint256(u.publicInputs[24]) != uint256(aud) >> 128 - || uint256(u.publicInputs[25]) != uint256(aud) & type(uint128).max - ) { - revert WrongAudience(); - } - - // `u.email` was already validated against the proof's packed public - // inputs above (lines marked "email matches packed") — return it - // verbatim so the Registry can use it as the display handle. `u.sub` - // is the immutable id identity binds on. - handle = u.email; - sessionKey = u.sessionKey; - expiresAt = jwtExp; - userId = u.sub; - } - - // ─── Internal: shared crypto / decode primitives ────────────── - function _verifyLeaf(bytes32[] memory path, bytes32 root, string memory prefix, bytes memory value) internal pure { - if (path.length > 32) revert PathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - if (!_merkleVerifyMem(path, root, leaf)) revert MerkleMismatch(); - } - - function _merkleVerifyMem(bytes32[] memory path, bytes32 root, bytes32 leaf) internal pure returns (bool) { - bytes32 cur = leaf; - for (uint256 i = 0; i < path.length; i++) { - cur = _hashPair(cur, path[i]); - } - return cur == root; - } - - function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - function _containsKeyValue(bytes memory haystack, bytes memory key, bytes memory value) - internal - pure - returns (bool) - { - return _contains(haystack, abi.encodePacked('"', key, '":"', value, '"')) - || _contains(haystack, abi.encodePacked('"', key, '": "', value, '"')); - } - - function _contains(bytes memory haystack, bytes memory needle) internal pure returns (bool) { - uint256 hLen = haystack.length; - uint256 nLen = needle.length; - if (nLen == 0) return true; - if (nLen > hLen) return false; - for (uint256 i = 0; i + nLen <= hLen; i++) { - bool m = true; - for (uint256 j = 0; j < nLen; j++) { - if (haystack[i + j] != needle[j]) { - m = false; - break; - } - } - if (m) return true; - } - return false; - } - - function _b64urlDecode(bytes memory input) internal pure returns (bytes memory) { - uint256 len = input.length; - while (len > 0 && uint8(input[len - 1]) == 0x3d) len--; - uint256 fullChunks = len / 4; - uint256 tail = len % 4; - if (tail == 1) revert InvalidB64Length(); - uint256 outLen = fullChunks * 3 + (tail == 0 ? 0 : tail - 1); - bytes memory out = new bytes(outLen); - uint256 outIdx = 0; - uint256 inIdx = 0; - for (uint256 i = 0; i < fullChunks; i++) { - uint256 b0 = _b64char(input[inIdx]); - uint256 b1 = _b64char(input[inIdx + 1]); - uint256 b2 = _b64char(input[inIdx + 2]); - uint256 b3 = _b64char(input[inIdx + 3]); - inIdx += 4; - out[outIdx] = bytes1(uint8((b0 << 2) | (b1 >> 4))); - out[outIdx + 1] = bytes1(uint8(((b1 & 0xf) << 4) | (b2 >> 2))); - out[outIdx + 2] = bytes1(uint8(((b2 & 0x3) << 6) | b3)); - outIdx += 3; - } - if (tail == 2) { - uint256 b0 = _b64char(input[inIdx]); - uint256 b1 = _b64char(input[inIdx + 1]); - out[outIdx] = bytes1(uint8((b0 << 2) | (b1 >> 4))); - } else if (tail == 3) { - uint256 b0 = _b64char(input[inIdx]); - uint256 b1 = _b64char(input[inIdx + 1]); - uint256 b2 = _b64char(input[inIdx + 2]); - out[outIdx] = bytes1(uint8((b0 << 2) | (b1 >> 4))); - out[outIdx + 1] = bytes1(uint8(((b1 & 0xf) << 4) | (b2 >> 2))); - } - return out; - } - - function _b64char(bytes1 c) internal pure returns (uint256) { - uint8 v = uint8(c); - if (v >= 65 && v <= 90) return v - 65; - if (v >= 97 && v <= 122) return v - 71; - if (v >= 48 && v <= 57) return uint256(v) + 4; - if (v == 45) return 62; - if (v == 95) return 63; - revert InvalidB64Char(); - } - - function _splitLimbs(bytes memory be) internal pure returns (bytes32[18] memory limbs) { - uint256[8] memory words; - assembly { - let beOffset := add(be, 32) - for { let i := 0 } lt(i, 8) { i := add(i, 1) } { - mstore(add(words, mul(i, 32)), mload(add(beOffset, mul(i, 32)))) - } - } - for (uint256 k = 0; k < 18; k++) { - uint256 startBit = k * 120; - uint256 endBit = startBit + 120; - if (endBit > 2048) endBit = 2048; - uint256 width = endBit - startBit; - uint256 startWord = startBit / 256; - uint256 endWord = (endBit - 1) / 256; - uint256 lv; - if (startWord == endWord) { - uint256 w = words[7 - startWord]; - lv = (w >> (startBit % 256)) & ((uint256(1) << width) - 1); - } else { - uint256 lowWord = words[7 - startWord]; - uint256 highWord = words[7 - endWord]; - uint256 lowShift = startBit % 256; - uint256 lowBits = 256 - lowShift; - uint256 highBits = width - lowBits; - uint256 lowVal = lowWord >> lowShift; - uint256 highMask = (uint256(1) << highBits) - 1; - uint256 highVal = (highWord & highMask) << lowBits; - lv = lowVal | highVal; - } - limbs[k] = bytes32(lv); - } - } - - function _padTo62(bytes memory s) internal pure returns (bytes memory out) { - require(s.length <= 62, "input >62 bytes"); - out = new bytes(62); - for (uint256 i = 0; i < s.length; i++) { - out[i] = s[i]; - } - } - - function _pack62(bytes memory p) internal pure returns (bytes32 hi, bytes32 lo) { - require(p.length == 62, "not 62"); - uint256 h; - uint256 l; - for (uint256 i = 0; i < 31; i++) { - h = h * 256 + uint8(p[i]); - l = l * 256 + uint8(p[i + 31]); - } - hi = bytes32(h); - lo = bytes32(l); - } - - function _padTo31(bytes memory s) internal pure returns (bytes memory out) { - require(s.length <= 31, "input >31 bytes"); - out = new bytes(31); - for (uint256 i = 0; i < s.length; i++) { - out[i] = s[i]; - } - } - - function _pack31(bytes memory p) internal pure returns (bytes32 packed) { - require(p.length == 31, "not 31"); - uint256 v; - for (uint256 i = 0; i < 31; i++) { - v = v * 256 + uint8(p[i]); - } - packed = bytes32(v); - } - - function _addressToAscii(address a) internal pure returns (bytes memory) { - bytes memory out = new bytes(42); - out[0] = "0"; - out[1] = "x"; - bytes16 hexc = "0123456789abcdef"; - uint160 v = uint160(a); - for (uint256 i = 0; i < 20; i++) { - uint8 b = uint8(v >> ((19 - i) * 8)); - out[2 + i * 2] = hexc[b >> 4]; - out[2 + i * 2 + 1] = hexc[b & 0x0f]; - } - return out; - } -} diff --git a/solidity/archived/login/oidc/IOidcVerifier.sol b/solidity/archived/login/oidc/IOidcVerifier.sol deleted file mode 100644 index fa3b8155..00000000 --- a/solidity/archived/login/oidc/IOidcVerifier.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -/// Plug-in OIDC identity-attestation module for the `Registry`. -/// -/// One implementation per OIDC provider (Google, Telegram, …). Each module -/// owns whatever trust roots its provider exposes (RSA JWKS) and the -/// matching UltraHonk verifier for that provider's JWT circuit. The -/// Registry stays small — it dispatches by `platform`, lets the verifier -/// validate the proof, and writes the resulting identity → wallet mapping -/// using the same code path that backs the TLSN-based X / GitHub flows. -interface IOidcVerifier { - /// Verify a user proof and return the validated identity. - /// - /// `proof` is platform-specific bytes (the verifier decodes them). - /// Returns: - /// * `handle` — human-readable identity from the JWT (e.g. email - /// for Google). The verifier guarantees this equals - /// the value bound inside the circuit's public - /// inputs — the Registry can use it directly as the - /// handle in its `(platform, handle) → wallet` map. - /// * `sessionKey` — Ethereum address the user is binding to. - /// * `expiresAt` — JWT validity deadline (unix seconds). - /// * `userId` — immutable platform user-id (Google JWT `sub`). - /// Escrow keys on this, never the mutable handle. - function verifyAndExtract(bytes calldata proof) - external - view - returns (string memory handle, address sessionKey, uint256 expiresAt, string memory userId); - - /// Process a notary-attested rotation of this verifier's trust roots. - /// For Google OIDC: TLS-notary attestation over the JWKS endpoint. - function rotateRoots(bytes calldata rotationProof) external; - - /// Human-readable platform name (e.g. `"google"`). Used for events - /// and debugging. - function platformName() external view returns (string memory); -} diff --git a/solidity/archived/login/oidc/Verifier.sol b/solidity/archived/login/oidc/Verifier.sol deleted file mode 100644 index 26a970ec..00000000 --- a/solidity/archived/login/oidc/Verifier.sol +++ /dev/null @@ -1,2461 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Aztec -pragma solidity >=0.8.21; - -uint256 constant N = 262144; -uint256 constant LOG_N = 18; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 36; -uint256 constant VK_HASH = 0x1a1fad94a1ecd83ce953541e1a6e24fc61a62f846bcd31f7e65ed26ad7d6ba08; - -library HonkVerificationKey { - function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { - Honk.VerificationKey memory vk = Honk.VerificationKey({ - circuitSize: uint256(262144), - logCircuitSize: uint256(18), - publicInputsSize: uint256(36), - ql: Honk.G1Point({ - x: uint256(0x1d5eccf7beb2bd6e4d6139fc7030dd30fd9ac6288360a972f9d3e093663b9749), - y: uint256(0x2daf3911a1eb09482454ef198bd85f37d76519ba230e812c687f2a107635b4a8) - }), - qr: Honk.G1Point({ - x: uint256(0x17c25693a8e14f7ebdddd748fb3215cd858d03d3f19d04976bba379082b5d528), - y: uint256(0x1da040a6ed2c59e00277a0e0e39d5adf7f871cde30d0fa1183a47bc321fe8f67) - }), - qo: Honk.G1Point({ - x: uint256(0x2991b455012204251bf866220a55808213c0ed0ede10c5119bcf3a0de3756f41), - y: uint256(0x1d3c2e52f3cd1b82749d5c5d0a87b4061084282be192f6600876035b09fa7cf5) - }), - q4: Honk.G1Point({ - x: uint256(0x1071e060db2aa32a11c888a07f0e286536d0a83ecc64c5f4605bd99f801c9ff1), - y: uint256(0x2fed5ba63f937e80ed923f176bdb7a9b9d403ebb12c7682d60573299f927161c) - }), - qm: Honk.G1Point({ - x: uint256(0x1cfa39cf0fa544b5de7a08554742edaa0ec2762256fc58c503a969ac1dd73719), - y: uint256(0x1349d33245f526c20fe48776dcb3776c68fe2522cc14534f7f159d5796e33a17) - }), - qc: Honk.G1Point({ - x: uint256(0x0f257837a5102e98c50c5ec28ce3973cac5f8fb8bf18f0f4dd46eaf635c81550), - y: uint256(0x2ffc8faa0b09583cf2a270ddd6e017d2d0061c4c7e57533d04ee9830babfd8a5) - }), - qLookup: Honk.G1Point({ - x: uint256(0x09e57c9e52e810c8b8533d1a3954bf01a660071ad560ce125c23eac06320f405), - y: uint256(0x2bc108ca74813679dacc6b0fa539c94a285cd646163cb2ce63d007e87d6b54ae) - }), - qArith: Honk.G1Point({ - x: uint256(0x15dc06ed9016b236564dbf6515dbd3506617b4fbe15524265164af93666964ee), - y: uint256(0x257ad06402f632f0e7eed9af6a8d3aadf30f7be46fb634b2db5260cfbfb6ed5a) - }), - qDeltaRange: Honk.G1Point({ - x: uint256(0x074bd7465151c3df24ac2a25dfab519bd8ed135f56272d72a3d946f4d0ccba37), - y: uint256(0x178a9a8f36aa145fb9c3ad36a22d7a3ef937b7e4a2241bc40eade0410c37ab3f) - }), - qElliptic: Honk.G1Point({ - x: uint256(0x04df8ec8779d604c3220ac2ee3974ec94fe2582f50f9463b02e82b9ec0040931), - y: uint256(0x1c432c588f21994deec3a64950555a4b55d5b889b5332e02f421cfe621d31fd8) - }), - qMemory: Honk.G1Point({ - x: uint256(0x10deefe499f9420af695ffdb4771395ff3481f0ded5ebb10a729f1c10c82e269), - y: uint256(0x2da66cc0eb84e697b92260e588270c415a14e6288015d4722aa249bc6253e3eb) - }), - qNnf: Honk.G1Point({ - x: uint256(0x1f556827bad8c3db1feb3461a344a9d2a9f697d1be0dd224ff9d9acd942fa423), - y: uint256(0x2bb88d4e6f7acdcfbc3f380a38c4136ddd06aa0307478b14ab4fb93d0a0bbb29) - }), - qPoseidon2External: Honk.G1Point({ - x: uint256(0x0d945eb0b5c0239afcc778b403ee0235f37bc2c9049e337e5acc627c5ae42210), - y: uint256(0x094d03a032417e0ae6bece072f8af6b903d051505c11cc75722a24b595dc630b) - }), - qPoseidon2Internal: Honk.G1Point({ - x: uint256(0x1af09c3bc978beb8d0df495a18c739477e109a84acc5d7537e788b809fd99277), - y: uint256(0x161466f6a46f985b36026cd4621e50230245a6c8ad922d31f2f4851f9d887b0f) - }), - s1: Honk.G1Point({ - x: uint256(0x102c88e39baca9594e1b915b4c7c63cf5de6399e3166f8bd33493f6668f92c7b), - y: uint256(0x28df7c0f723f057fdf4730c96a66f6ff91ff81799e984fb440940ee923621b6d) - }), - s2: Honk.G1Point({ - x: uint256(0x2044a388617a3a282e2754a32ac0eb4cfa5476a2bc3a0d6b09ddd176d41207df), - y: uint256(0x150d72a920a8d889ff16ce2c019e51068ffb70825bf2ace436a2b1cfd13d9c5f) - }), - s3: Honk.G1Point({ - x: uint256(0x258ddf4bc87075a4485f97eda24c622b55d945323a6a0258aa962ca278c8546d), - y: uint256(0x29fdc2372e9744f56b054dfc1ce82f7ec9b446cb2e3d359c53d62eb8f4dcff61) - }), - s4: Honk.G1Point({ - x: uint256(0x25cdc5c257ae3248de950263f733c0e77d38b6361ce5bdee745cd9d5411e4f0e), - y: uint256(0x0bf945904c720b62e02bf58b932b42e40b0bb0cf51fdae95bad4f16a5f58d57f) - }), - t1: Honk.G1Point({ - x: uint256(0x26814a59803555635a3b6dfd056d19f4a6c6ffdf80a72db78762e237d58cd5f6), - y: uint256(0x0b9dc32399025796005b2f68c15991f45b8d96069231f48e0d0c34d57593a6a6) - }), - t2: Honk.G1Point({ - x: uint256(0x1f6cd5b6d43f67798ae4655c016f37750e1b5cb997a3adf8540bf55bb9264ec4), - y: uint256(0x0ed62d10b019355f008b46412d0e2d596cc6183848f32c60ec74fe2bcd76b20f) - }), - t3: Honk.G1Point({ - x: uint256(0x23a2906d03204aa4f79b5b80b92eaa2020dfdab8c9db8335bca3c8284b1585b2), - y: uint256(0x24aecae332db48e03b97fc645a7f059116c4d15f7c9a51f17c3830fe7c1e3b42) - }), - t4: Honk.G1Point({ - x: uint256(0x08524b40cf87b668119022f691cfac834c5e938cf9e8bd4bd5847e2408407276), - y: uint256(0x0e1e9d4327e635f547da67aef57e86e699df860731437ec57f9890fe4b3a4205) - }), - id1: Honk.G1Point({ - x: uint256(0x0fc9b284799173bff317ebe1c893ead71243e5b0ae8d2b39697b10fff7c4c1ef), - y: uint256(0x046a84610998570baaec81b831e5b203fe530f5bd8492e301a5eb3f64b6af92f) - }), - id2: Honk.G1Point({ - x: uint256(0x04e38bb550f66dd4cc0bf599150a83bad549f09ac5888a7f76ff5f5be82e33e6), - y: uint256(0x09f26e5bb2265759c68d02adb00491190f7207cc1689607ffa4a7d053be7021d) - }), - id3: Honk.G1Point({ - x: uint256(0x0c4b729451959d81da634887d88dbf8d9bfd0c6c062690df7e436bfe4cd1e76e), - y: uint256(0x20dab2b2a22c393923aea5ce27a953cec07399aff6d5c42f05b0d0653ff524be) - }), - id4: Honk.G1Point({ - x: uint256(0x270a1d39c76987698ed303ea31b4ccb37af98f03c0c0ea1df105af1a2cf34530), - y: uint256(0x1004ba82c884208931427cda4d093d81039f1a352c09d672bc11f6ec50442168) - }), - lagrangeFirst: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) - }), - lagrangeLast: Honk.G1Point({ - x: uint256(0x2a65a82f2cd8853c1548dd31f29785ad1d1c716aa72970dd54aff5adce5014ec), - y: uint256(0x2c3ddf59d3904025f1412bba2aef0a9da0bbb5b22d9dcdbca7a97c0cdacd8f47) - }) - }); - return vk; - } -} - -pragma solidity ^0.8.27; - -interface IVerifier { - function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); -} - -/** - * @notice Library of error codes - * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier - */ -library Errors { - error ValueGeLimbMax(); - error ValueGeGroupOrder(); - error ValueGeFieldOrder(); - - error InvertOfZero(); - error NotPowerOfTwo(); - error ModExpFailed(); - - error ProofLengthWrong(); - error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); - error PublicInputsLengthWrong(); - error SumcheckFailed(); - error ShpleminiFailed(); - - error PointAtInfinity(); - - error ConsistencyCheckFailed(); - error GeminiChallengeInSubgroup(); -} - -type Fr is uint256; - -using {add as +} for Fr global; -using {sub as -} for Fr global; -using {mul as *} for Fr global; - -using {notEqual as !=} for Fr global; -using {equal as ==} for Fr global; - -uint256 constant SUBGROUP_SIZE = 256; -uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order -uint256 constant P = MODULUS; -Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); -Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); -Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); -Fr constant ONE = Fr.wrap(1); -Fr constant ZERO = Fr.wrap(0); -// Instantiation - -library FrLib { - bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; - - function invert(Fr value) internal view returns (Fr) { - uint256 v = Fr.unwrap(value); - require(v != 0, Errors.InvertOfZero()); - - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), v) - mstore(add(free, 0x80), sub(MODULUS, 2)) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function pow(Fr base, uint256 v) internal view returns (Fr) { - uint256 b = Fr.unwrap(base); - // Only works for power of 2 - require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), b) - mstore(add(free, 0x80), v) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function div(Fr numerator, Fr denominator) internal view returns (Fr) { - unchecked { - return numerator * invert(denominator); - } - } - - function sqr(Fr value) internal pure returns (Fr) { - unchecked { - return value * value; - } - } - - function unwrap(Fr value) internal pure returns (uint256) { - unchecked { - return Fr.unwrap(value); - } - } - - function neg(Fr value) internal pure returns (Fr) { - unchecked { - return Fr.wrap(MODULUS - Fr.unwrap(value)); - } - } - - function from(uint256 value) internal pure returns (Fr) { - unchecked { - require(value < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(value); - } - } - - function fromBytes32(bytes32 value) internal pure returns (Fr) { - unchecked { - uint256 v = uint256(value); - require(v < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(v); - } - } - - function toBytes32(Fr value) internal pure returns (bytes32) { - unchecked { - return bytes32(Fr.unwrap(value)); - } - } -} - -// Free functions -function add(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function mul(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function sub(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); - } -} - -function notEqual(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) != Fr.unwrap(b); - } -} - -function equal(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) == Fr.unwrap(b); - } -} - -uint256 constant CONST_PROOF_SIZE_LOG_N = 25; - -uint256 constant NUMBER_OF_SUBRELATIONS = 28; -uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; -uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; -uint256 constant NUMBER_OF_ENTITIES = 41; -// The number of entities added for ZK (gemini_masking_poly) -uint256 constant NUM_MASKING_POLYNOMIALS = 1; -uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_UNSHIFTED = 36; -uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_TO_BE_SHIFTED = 5; -uint256 constant PAIRING_POINTS_SIZE = 8; - -uint256 constant FIELD_ELEMENT_SIZE = 0x20; -uint256 constant GROUP_ELEMENT_SIZE = 0x40; - -// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) -uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; - -// ENUM FOR WIRES -enum WIRE { - Q_M, - Q_C, - Q_L, - Q_R, - Q_O, - Q_4, - Q_LOOKUP, - Q_ARITH, - Q_RANGE, - Q_ELLIPTIC, - Q_MEMORY, - Q_NNF, - Q_POSEIDON2_EXTERNAL, - Q_POSEIDON2_INTERNAL, - SIGMA_1, - SIGMA_2, - SIGMA_3, - SIGMA_4, - ID_1, - ID_2, - ID_3, - ID_4, - TABLE_1, - TABLE_2, - TABLE_3, - TABLE_4, - LAGRANGE_FIRST, - LAGRANGE_LAST, - W_L, - W_R, - W_O, - W_4, - Z_PERM, - LOOKUP_INVERSES, - LOOKUP_READ_COUNTS, - LOOKUP_READ_TAGS, - W_L_SHIFT, - W_R_SHIFT, - W_O_SHIFT, - W_4_SHIFT, - Z_PERM_SHIFT -} - -library Honk { - struct G1Point { - uint256 x; - uint256 y; - } - - struct VerificationKey { - // Misc Params - uint256 circuitSize; - uint256 logCircuitSize; - uint256 publicInputsSize; - // Selectors - G1Point qm; - G1Point qc; - G1Point ql; - G1Point qr; - G1Point qo; - G1Point q4; - G1Point qLookup; // Lookup - G1Point qArith; // Arithmetic widget - G1Point qDeltaRange; // Delta Range sort - G1Point qMemory; // Memory - G1Point qNnf; // Non-native Field - G1Point qElliptic; // Auxillary - G1Point qPoseidon2External; - G1Point qPoseidon2Internal; - // Copy constraints - G1Point s1; - G1Point s2; - G1Point s3; - G1Point s4; - // Copy identity - G1Point id1; - G1Point id2; - G1Point id3; - G1Point id4; - // Precomputed lookup table - G1Point t1; - G1Point t2; - G1Point t3; - G1Point t4; - // Fixed first and last - G1Point lagrangeFirst; - G1Point lagrangeLast; - } - - struct RelationParameters { - // challenges - Fr eta; - Fr beta; - Fr gamma; - // derived - Fr publicInputsDelta; - } - - struct Proof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // Free wires - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Lookup helpers - Permutations - G1Point zPerm; - // Lookup helpers - logup - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Sumcheck - Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - G1Point shplonkQ; - G1Point kzgQuotient; - } - - /// forge-lint: disable-next-item(pascal-case-struct) - struct ZKProof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) - G1Point geminiMaskingPoly; - // Commitments to wire polynomials - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Commitments to logup witness polynomials - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Commitment to grand permutation polynomial - G1Point zPerm; - G1Point[3] libraCommitments; - // Sumcheck - Fr libraSum; - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr libraEvaluation; - Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - Fr[4] libraPolyEvals; - G1Point shplonkQ; - G1Point kzgQuotient; - } -} - -// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest -/// forge-lint: disable-next-item(pascal-case-struct) -struct ZKTranscript { - // Oink - Honk.RelationParameters relationParameters; - Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] - Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; - // Sumcheck - Fr libraChallenge; - Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; - // Shplemini - Fr rho; - Fr geminiR; - Fr shplonkNu; - Fr shplonkZ; - // Derived - Fr publicInputsDelta; -} - -library ZKTranscriptLib { - function generateTranscript( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - uint256 logN - ) external pure returns (ZKTranscript memory t) { - Fr previousChallenge; - (t.relationParameters, previousChallenge) = - generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); - - (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); - - (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); - (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); - (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); - - (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); - - (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); - - (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); - - (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); - return t; - } - - function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { - uint256 challengeU256 = uint256(Fr.unwrap(challenge)); - // Split into two equal 127-bit chunks (254/2) - uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits - uint256 hi = challengeU256 >> 127; - first = FrLib.from(lo); - second = FrLib.from(hi); - } - - function generateRelationParametersChallenges( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - Fr previousChallenge - ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { - (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); - - (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); - } - - function generateEtaChallenge( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize - ) internal pure returns (Fr eta, Fr previousChallenge) { - // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) - bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); - round0[0] = bytes32(vkHash); - - for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { - require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); - round0[1 + i] = publicInputs[i]; - } - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); - } - - // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) - round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); - round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); - - // Create the first challenge - // Note: w4 is added to the challenge later on - round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); - round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); - round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); - round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); - round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); - round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); - - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); - (eta,) = splitChallenge(previousChallenge); - } - - function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) - { - bytes32[7] memory round1; - round1[0] = FrLib.toBytes32(previousChallenge); - round1[1] = bytes32(proof.lookupReadCounts.x); - round1[2] = bytes32(proof.lookupReadCounts.y); - round1[3] = bytes32(proof.lookupReadTags.x); - round1[4] = bytes32(proof.lookupReadTags.y); - round1[5] = bytes32(proof.w4.x); - round1[6] = bytes32(proof.w4.y); - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); - (beta, gamma) = splitChallenge(nextPreviousChallenge); - } - - // Alpha challenges non-linearise the gate contributions - function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) - { - // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup - uint256[5] memory alpha0; - alpha0[0] = Fr.unwrap(previousChallenge); - alpha0[1] = proof.lookupInverses.x; - alpha0[2] = proof.lookupInverses.y; - alpha0[3] = proof.zPerm.x; - alpha0[4] = proof.zPerm.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); - Fr alpha; - (alpha,) = splitChallenge(nextPreviousChallenge); - - // Compute powers of alpha for batching subrelations - alphas[0] = alpha; - for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { - alphas[i] = alphas[i - 1] * alpha; - } - } - - function generateGateChallenges(Fr previousChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) - { - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); - (gateChallenges[0],) = splitChallenge(previousChallenge); - for (uint256 i = 1; i < logN; i++) { - gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; - } - nextPreviousChallenge = previousChallenge; - } - - function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr libraChallenge, Fr nextPreviousChallenge) - { - // 2 comm, 1 sum, 1 challenge - uint256[4] memory challengeData; - challengeData[0] = Fr.unwrap(previousChallenge); - challengeData[1] = proof.libraCommitments[0].x; - challengeData[2] = proof.libraCommitments[0].y; - challengeData[3] = Fr.unwrap(proof.libraSum); - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); - (libraChallenge,) = splitChallenge(nextPreviousChallenge); - } - - function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) - { - for (uint256 i = 0; i < logN; i++) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; - univariateChal[0] = prevChallenge; - - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; - } - prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); - - (sumcheckChallenges[i],) = splitChallenge(prevChallenge); - } - nextPreviousChallenge = prevChallenge; - } - - // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) - function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr rho, Fr nextPreviousChallenge) - { - uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; - rhoChallengeElements[0] = Fr.unwrap(prevChallenge); - uint256 i; - for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { - rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); - } - rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); - i += 1; - rhoChallengeElements[i] = proof.libraCommitments[1].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; - i += 2; - rhoChallengeElements[i] = proof.libraCommitments[2].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); - (rho,) = splitChallenge(nextPreviousChallenge); - } - - function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr geminiR, Fr nextPreviousChallenge) - { - uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); - gR[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 0; i < logN - 1; i++) { - gR[1 + i * 2] = proof.geminiFoldComms[i].x; - gR[2 + i * 2] = proof.geminiFoldComms[i].y; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); - - (geminiR,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr shplonkNu, Fr nextPreviousChallenge) - { - uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); - shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 1; i <= logN; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); - } - - uint256 libraIdx = 0; - for (uint256 i = logN + 1; i <= logN + 4; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); - libraIdx++; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); - (shplonkNu,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr shplonkZ, Fr nextPreviousChallenge) - { - uint256[3] memory shplonkZChallengeElements; - shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); - - shplonkZChallengeElements[1] = proof.shplonkQ.x; - shplonkZChallengeElements[2] = proof.shplonkQ.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); - (shplonkZ,) = splitChallenge(nextPreviousChallenge); - } - - function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { - uint256 boundary = 0x0; - - // Pairing point object - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); - // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 - require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); - p.pairingPointObject[i] = FrLib.from(limb); - boundary += FIELD_ELEMENT_SIZE; - } - - // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) - p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Commitments - p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Lookup / Permutation Helper Commitments - p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - // Sumcheck univariates - for (uint256 i = 0; i < logN; i++) { - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - } - - // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) - for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { - p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - - p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Gemini - // Read gemini fold univariates - for (uint256 i = 0; i < logN - 1; i++) { - p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - } - - // Read gemini a evaluations - for (uint256 i = 0; i < logN; i++) { - p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - for (uint256 i = 0; i < 4; i++) { - p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - // Shplonk - p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - // KZG - p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - } -} - -library RelationsLib { - struct EllipticParams { - // Points - Fr x_1; - Fr y_1; - Fr x_2; - Fr y_2; - Fr y_3; - Fr x_3; - // push accumulators into memory - Fr x_double_identity; - } - - // Parameters used within the Memory Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct MemParams { - Fr memory_record_check; - Fr partial_record_check; - Fr next_gate_access_type; - Fr record_delta; - Fr index_delta; - Fr adjacent_values_match_if_adjacent_indices_match; - Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; - Fr access_check; - Fr next_gate_access_type_is_boolean; - Fr ROM_consistency_check_identity; - Fr RAM_consistency_check_identity; - Fr timestamp_delta; - Fr RAM_timestamp_check_identity; - Fr memory_identity; - Fr index_is_monotonically_increasing; - } - - // Parameters used within the Non-Native Field Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct NnfParams { - Fr limb_subproduct; - Fr non_native_field_gate_1; - Fr non_native_field_gate_2; - Fr non_native_field_gate_3; - Fr limb_accumulator_1; - Fr limb_accumulator_2; - Fr nnf_identity; - } - - struct PoseidonExternalParams { - Fr s1; - Fr s2; - Fr s3; - Fr s4; - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr t0; - Fr t1; - Fr t2; - Fr t3; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr q_pos_by_scaling; - } - - struct PoseidonInternalParams { - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr u_sum; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr s1; - Fr q_pos_by_scaling; - } - - Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) - uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; - - // Constants for the Non-native Field relation - Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); - Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); - - function accumulateRelationEvaluations( - Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, - Fr powPartialEval - ) internal pure returns (Fr accumulator) { - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; - - // Accumulate all relations in Ultra Honk - each with varying number of subrelations - accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); - - // batch the subrelations with the precomputed alpha powers to obtain the full honk relation - accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); - } - - /** - * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids - * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code - * editors, and thus is noisy. - */ - function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { - return p[uint256(_wire)]; - } - - /** - * Ultra Arithmetic Relation - * - */ - function accumulateArithmeticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - // Relation 0 - Fr q_arith = wire(p, WIRE.Q_ARITH); - { - Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); - - Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; - accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) - + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); - accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); - accum = accum * q_arith; - accum = accum * domainSep; - evals[0] = accum; - } - - // Relation 1 - { - Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); - accum = accum * (q_arith - Fr.wrap(2)); - accum = accum * (q_arith - ONE); - accum = accum * q_arith; - accum = accum * domainSep; - evals[1] = accum; - } - } - - function accumulatePermutationRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr grand_product_numerator; - Fr grand_product_denominator; - - { - Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; - num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); - - grand_product_numerator = num; - } - { - Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; - den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); - - grand_product_denominator = den; - } - - // Contribution 2 - { - Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; - - acc = acc - - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) - * grand_product_denominator); - acc = acc * domainSep; - evals[2] = acc; - } - - // Contribution 3 - { - Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; - evals[3] = acc; - } - } - - function accumulateLogDerivativeLookupRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr table_term; - Fr lookup_term; - - // Calculate the write term (the table accumulation) - // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) - + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); - } - - // Calculate the read term - // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); - Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); - Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); - - lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) - + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); - } - - Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; - Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; - - Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) - - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); - - // Inverse calculated correctly relation - Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; - accumulatorNone = accumulatorNone * domainSep; - - // Inverse - Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; - - Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); - - Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; - - evals[4] = accumulatorNone; - evals[5] = accumulatorOne; - evals[6] = read_tag_boolean_relation * domainSep; - } - - function accumulateDeltaRangeRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr minus_one = ZERO - ONE; - Fr minus_two = ZERO - Fr.wrap(2); - Fr minus_three = ZERO - Fr.wrap(3); - - // Compute wire differences - Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); - Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); - Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); - Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); - - // Contribution 6 - { - Fr acc = delta_1; - acc = acc * (delta_1 + minus_one); - acc = acc * (delta_1 + minus_two); - acc = acc * (delta_1 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[7] = acc; - } - - // Contribution 7 - { - Fr acc = delta_2; - acc = acc * (delta_2 + minus_one); - acc = acc * (delta_2 + minus_two); - acc = acc * (delta_2 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[8] = acc; - } - - // Contribution 8 - { - Fr acc = delta_3; - acc = acc * (delta_3 + minus_one); - acc = acc * (delta_3 + minus_two); - acc = acc * (delta_3 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[9] = acc; - } - - // Contribution 9 - { - Fr acc = delta_4; - acc = acc * (delta_4 + minus_one); - acc = acc * (delta_4 + minus_two); - acc = acc * (delta_4 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[10] = acc; - } - } - - function accumulateEllipticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - EllipticParams memory ep; - ep.x_1 = wire(p, WIRE.W_R); - ep.y_1 = wire(p, WIRE.W_O); - - ep.x_2 = wire(p, WIRE.W_L_SHIFT); - ep.y_2 = wire(p, WIRE.W_4_SHIFT); - ep.y_3 = wire(p, WIRE.W_O_SHIFT); - ep.x_3 = wire(p, WIRE.W_R_SHIFT); - - Fr q_sign = wire(p, WIRE.Q_L); - Fr q_is_double = wire(p, WIRE.Q_M); - - // Contribution 10 point addition, x-coordinate check - // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 - Fr x_diff = (ep.x_2 - ep.x_1); - Fr y1_sqr = (ep.y_1 * ep.y_1); - { - // Move to top - Fr partialEval = domainSep; - - Fr y2_sqr = (ep.y_2 * ep.y_2); - Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; - Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); - x_add_identity = x_add_identity * x_diff * x_diff; - x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; - - evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 11 point addition, x-coordinate check - // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 - { - Fr y1_plus_y3 = ep.y_1 + ep.y_3; - Fr y_diff = ep.y_2 * q_sign - ep.y_1; - Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; - evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 10 point doubling, x-coordinate check - // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 - // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 - { - Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; - Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; - y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; - Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); - - // NOTE: pushed into memory (stack >:'( ) - ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; - - Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - evals[11] = evals[11] + acc; - } - - // Contribution 11 point doubling, y-coordinate check - // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 - { - Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; - Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); - evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - } - } - - function accumulateMemoryRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - MemParams memory ap; - - // Compute eta powers locally - Fr eta_two = rp.eta * rp.eta; - Fr eta_three = eta_two * rp.eta; - - /** - * MEMORY - * - * A RAM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) - * * v: `value` of memory cell being accessed - * * a: `access` type of record. read: 0 = read, 1 = write - * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three - * - * A ROM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three - * - * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + - * selectors, depending on whether the gate is a RAM read/write or a ROM read - * - * | gate type | i | v2/t | v | a | r | - * | --------- | -- | ----- | -- | -- | -- | - * | ROM | w1 | w2 | w3 | -- | w4 | - * | RAM | w1 | w2 | w3 | qc | w4 | - * - * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on - * `w2` to fix its value) - * - * - */ - - /** - * Memory Record Check - * Partial degree: 1 - * Total degree: 4 - * - * A ROM/ROM access gate can be evaluated with the identity: - * - * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 - * - * For ROM gates, qc = 0 - */ - ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); - ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); - ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 - ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); - - /** - * Contribution 13 & 14 - * ROM Consistency Check - * Partial degree: 1 - * Total degree: 4 - * - * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of - * records that are sorted. - * - * We apply the following checks for the sorted records: - * - * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 - * 2. index values for adjacent records are monotonically increasing - * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} - * - */ - ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); - ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); - - ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 - - ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 - - evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - - ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 - - /** - * Contributions 15,16,17 - * RAM Consistency Check - * - * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` - * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. - * This is validated by requiring `access` to be boolean - * - * For two adjacent entries in the sorted list if _both_ - * A) index values match - * B) adjacent access value is 0 (i.e. next gate is a READ) - * then - * C) both values must match. - * The gate boolean check is - * (A && B) => C === !(A && B) || C === !A || !B || C - * - * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized - * with a WRITE operation. - */ - Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 - ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 - - // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta - // deg 1 or 4 - ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); - ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; - - Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); - ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = - (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 - - // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the - // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't - // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access - // type is correct, to cover this edge case - // deg 2 or 4 - ap.next_gate_access_type_is_boolean = - ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; - - // Putting it all together... - evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation - * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 - evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 - evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 - - ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 - - /** - * RAM Timestamp Consistency Check - * - * | w1 | w2 | w3 | w4 | - * | index | timestamp | timestamp_check | -- | - * - * Let delta_index = index_{i + 1} - index_{i} - * - * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i - * Else timestamp_check = 0 - */ - ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); - ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 - - /** - * Complete Contribution 12 - * The complete RAM/ROM memory identity - * Partial degree: - */ - ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 - ap.memory_identity = - ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 - ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 - ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 - - // (deg 3 or 9) + (deg 4) + (deg 3) - ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 - evals[13] = ap.memory_identity; - } - - function accumulateNnfRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - NnfParams memory ap; - - /** - * Contribution 12 - * Non native field arithmetic gate 2 - * deg 4 - * - * _ _ - * / _ _ _ 14 \ - * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | - * \_ _/ - * - * - */ - ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); - ap.non_native_field_gate_2 = - (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); - - ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; - ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); - ap.non_native_field_gate_1 = ap.limb_subproduct; - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); - - ap.non_native_field_gate_3 = ap.limb_subproduct; - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); - - Fr non_native_field_identity = - ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; - non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); - - // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm - // deg 2 - ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); - ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); - - // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm - // deg 2 - ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); - ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); - - Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; - limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 - - ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; - ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); - evals[19] = ap.nnf_identity; - } - - function accumulatePoseidonExternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonExternalParams memory ep; - - ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); - ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); - ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); - - ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; - ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; - ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; - ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; - // matrix mul v = M_E * u with 14 additions - ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 - ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 - ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 - // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 - ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 - // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 - ep.v4 = ep.t1 + ep.t1; - ep.v4 = ep.v4 + ep.v4 + ep.t3; - // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 - ep.v2 = ep.t0 + ep.t0; - ep.v2 = ep.v2 + ep.v2 + ep.t2; - // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 - ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 - ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 - - ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; - evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); - - evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); - - evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); - - evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - function accumulatePoseidonInternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonInternalParams memory ip; - - Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ - FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), - FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), - FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), - FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) - ]; - - // add round constants - ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - - // apply s-box round - ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; - ip.u2 = wire(p, WIRE.W_R); - ip.u3 = wire(p, WIRE.W_O); - ip.u4 = wire(p, WIRE.W_4); - - // matrix mul with v = M_I * u 4 muls and 7 additions - ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; - - ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; - - ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; - evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); - - ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; - evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); - - ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; - evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); - - ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; - evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - // Batch subrelation evaluations using precomputed powers of alpha - // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array - function scaleAndBatchSubrelations( - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges - ) internal pure returns (Fr accumulator) { - accumulator = evaluations[0]; - - for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { - accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; - } - } -} - -library CommitmentSchemeLib { - using FrLib for Fr; - - // Avoid stack too deep - struct ShpleminiIntermediates { - Fr unshiftedScalar; - Fr shiftedScalar; - Fr unshiftedScalarNeg; - Fr shiftedScalarNeg; - // Scalar to be multiplied by [1]₁ - Fr constantTermAccumulator; - // Accumulator for powers of rho - Fr batchingChallenge; - // Linear combination of multilinear (sumcheck) evaluations and powers of rho - Fr batchedEvaluation; - Fr[4] denominators; - Fr[4] batchingScalars; - // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr posInvertedDenominator; - // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr negInvertedDenominator; - // ν^{2i} * 1/(z - r^{2^i}) - Fr scalingFactorPos; - // ν^{2i+1} * 1/(z + r^{2^i}) - Fr scalingFactorNeg; - // Fold_i(r^{2^i}) reconstructed by Verifier - Fr[] foldPosEvaluations; - } - - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 - function computeFoldPosEvaluations( - Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, - Fr batchedEvalAccumulator, - Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, - Fr[] memory geminiEvalChallengePowers, - uint256 logSize - ) internal view returns (Fr[] memory) { - Fr[] memory foldPosEvaluations = new Fr[](logSize); - for (uint256 i = logSize; i > 0; --i) { - Fr challengePower = geminiEvalChallengePowers[i - 1]; - Fr u = sumcheckUChallenges[i - 1]; - - Fr batchedEvalRoundAcc = - ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] - * (challengePower * (ONE - u) - u)); - // Divide by the denominator - batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); - - batchedEvalAccumulator = batchedEvalRoundAcc; - foldPosEvaluations[i - 1] = batchedEvalRoundAcc; - } - return foldPosEvaluations; - } - - function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { - Fr[] memory squares = new Fr[](logN); - squares[0] = r; - for (uint256 i = 1; i < logN; ++i) { - squares[i] = squares[i - 1].sqr(); - } - return squares; - } -} - -uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q - -// Fr utility - -function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { - scalar = FrLib.fromBytes32(bytes32(proofSection)); -} - -// EC Point utilities -function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { - uint256 x = uint256(bytes32(proofSection[0x00:0x20])); - uint256 y = uint256(bytes32(proofSection[0x20:0x40])); - require(x < Q && y < Q, Errors.ValueGeGroupOrder()); - - // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) - // as the identity element, which could zero out commitments. - // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles - // per EIP-196, so we only need to catch this special case here. - require((x | y) != 0, Errors.PointAtInfinity()); - - point = Honk.G1Point({x: x, y: y}); -} - -function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { - // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. - if (point.y != 0) { - point.y = Q - point.y; - } - return point; -} - -/** - * Convert the pairing points to G1 points. - * - * The pairing points are serialised as an array of 2 limbs representing two points - * (P0 and P1, used for lhs and rhs of pairing operation). - * - * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. - * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] - * - * @param pairingPoints The pairing points to convert. - * @return lhs P0 point - * @return rhs P1 point - */ -function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) - pure - returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) -{ - // P0 (lhs): x = lo | (hi << 136) - uint256 lhsX = Fr.unwrap(pairingPoints[0]); - lhsX |= Fr.unwrap(pairingPoints[1]) << 136; - - uint256 lhsY = Fr.unwrap(pairingPoints[2]); - lhsY |= Fr.unwrap(pairingPoints[3]) << 136; - - // P1 (rhs): x = lo | (hi << 136) - uint256 rhsX = Fr.unwrap(pairingPoints[4]); - rhsX |= Fr.unwrap(pairingPoints[5]) << 136; - - uint256 rhsY = Fr.unwrap(pairingPoints[6]); - rhsY |= Fr.unwrap(pairingPoints[7]) << 136; - - // Reconstructed coordinates must be < Q to prevent malleability. - // Without this, two different limb encodings could map to the same curve point - // (via mulmod reduction in on-curve checks) but produce different transcript hashes. - require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); - - lhs.x = lhsX; - lhs.y = lhsY; - rhs.x = rhsX; - rhs.y = rhsY; -} - -/** - * Hash the pairing inputs from the present verification context with those extracted from the public inputs. - * - * @param proofPairingPoints Pairing points from the proof - (public inputs). - * @param accLhs Accumulator point for the left side - result of shplemini. - * @param accRhs Accumulator point for the right side - result of shplemini. - * @return recursionSeparator The recursion separator - generated from hashing the above. - */ -function generateRecursionSeparator( - Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, - Honk.G1Point memory accLhs, - Honk.G1Point memory accRhs -) pure returns (Fr recursionSeparator) { - // hash the proof aggregated X - // hash the proof aggregated Y - // hash the accum X - // hash the accum Y - - (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); - - uint256[8] memory recursionSeparatorElements; - - // Proof points - recursionSeparatorElements[0] = proofLhs.x; - recursionSeparatorElements[1] = proofLhs.y; - recursionSeparatorElements[2] = proofRhs.x; - recursionSeparatorElements[3] = proofRhs.y; - - // Accumulator points - recursionSeparatorElements[4] = accLhs.x; - recursionSeparatorElements[5] = accLhs.y; - recursionSeparatorElements[6] = accRhs.x; - recursionSeparatorElements[7] = accRhs.y; - - recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); -} - -/** - * G1 Mul with Separator - * Using the ecAdd and ecMul precompiles - * - * @param basePoint The point to multiply. - * @param other The other point to add. - * @param recursionSeperator The separator to use for the multiplication. - * @return `(recursionSeperator * basePoint) + other`. - */ -function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) - view - returns (Honk.G1Point memory) -{ - Honk.G1Point memory result; - - result = ecMul(recursionSeperator, basePoint); - result = ecAdd(result, other); - - return result; -} - -/** - * G1 Mul - * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. - * - * @param value The value to multiply the point by. - * @param point The point to multiply. - * @return result The result of the multiplication. - */ -function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write the point into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | point.x - // free + 0x20| point.y - mstore(free, mload(point)) - mstore(add(free, 0x20), mload(add(point, 0x20))) - // Write the scalar into memory (one 32 byte word) - // Memory layout: - // Address | value - // free + 0x40| value - mstore(add(free, 0x40), value) - - // Call the ecMul precompile, it takes in the following - // [point.x, point.y, scalar], and returns the result back into the free memory location. - let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) - if iszero(success) { - revert(0, 0) - } - // Copy the result of the multiplication back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x60)) - } - - return result; -} - -/** - * G1 Add - * Takes two G1 points and uses the ecAdd precompile to return the result. - * - * @param lhs The left hand side of the addition. - * @param rhs The right hand side of the addition. - * @return result The result of the addition. - */ -function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write lhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | lhs.x - // free + 0x20| lhs.y - mstore(free, mload(lhs)) - mstore(add(free, 0x20), mload(add(lhs, 0x20))) - - // Write rhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free + 0x40| rhs.x - // free + 0x60| rhs.y - mstore(add(free, 0x40), mload(rhs)) - mstore(add(free, 0x60), mload(add(rhs, 0x20))) - - // Call the ecAdd precompile, it takes in the following - // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. - let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) - if iszero(success) { revert(0, 0) } - - // Copy the result of the addition back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x80)) - } - - return result; -} - -function rejectPointAtInfinity(Honk.G1Point memory point) pure { - require((point.x | point.y) != 0, Errors.PointAtInfinity()); -} - -/** - * Check if pairing point limbs are all zero (default/infinity). - * Default pairing points indicate no recursive verification occurred. - */ -function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { - uint256 acc = 0; - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - acc |= Fr.unwrap(pairingPoints[i]); - } - return acc == 0; -} - -function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { - bytes memory input = abi.encodePacked( - rhs.x, - rhs.y, - // Fixed G2 point - uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), - uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), - uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), - uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), - lhs.x, - lhs.y, - // G2 point from VK - uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), - uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), - uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), - uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) - ); - - (bool success, bytes memory result) = address(0x08).staticcall(input); - decodedResult = success && abi.decode(result, (bool)); -} - -abstract contract BaseZKHonkVerifier is IVerifier { - using FrLib for Fr; - - struct PairingInputs { - Honk.G1Point P_0; - Honk.G1Point P_1; - } - - struct SmallSubgroupIpaIntermediates { - Fr[SUBGROUP_SIZE] challengePolyLagrange; - Fr challengePolyEval; - Fr lagrangeFirst; - Fr lagrangeLast; - Fr rootPower; - Fr[SUBGROUP_SIZE] denominators; // this has to disappear - Fr diff; - } - - // Constants for proof length calculation (matching UltraKeccakZKFlavor) - uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; - uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points - uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements - uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations - - uint256 internal constant LIBRA_COMMITMENTS = 3; - uint256 internal constant LIBRA_EVALUATIONS = 4; - uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; - - uint256 internal constant SHIFTED_COMMITMENTS_START = 30; - uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; - - uint256 internal immutable $N; - uint256 internal immutable $LOG_N; - uint256 internal immutable $VK_HASH; - uint256 internal immutable $NUM_PUBLIC_INPUTS; - uint256 internal immutable $MSMSize; - - constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { - $N = _N; - $LOG_N = _logN; - $VK_HASH = _vkHash; - $NUM_PUBLIC_INPUTS = _numPublicInputs; - $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; - } - - function verify(bytes calldata proof, bytes32[] calldata publicInputs) - public - view - override - returns (bool verified) - { - // Calculate expected proof size based on $LOG_N - uint256 expectedProofSize = calculateProofSize($LOG_N); - - // Check the received proof is the expected size where each field element is 32 bytes - require( - proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) - ); - - Honk.VerificationKey memory vk = loadVerificationKey(); - Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); - - require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); - - // Generate the fiat shamir challenges for the whole protocol - ZKTranscript memory t = - ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); - - // Derive public input delta - t.relationParameters.publicInputsDelta = computePublicInputDelta( - publicInputs, - p.pairingPointObject, - t.relationParameters.beta, - t.relationParameters.gamma, /*pubInputsOffset=*/ - 1 - ); - - // Sumcheck - require(verifySumcheck(p, t), Errors.SumcheckFailed()); - require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); - - verified = true; - } - - function computePublicInputDelta( - bytes32[] memory publicInputs, - Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, - Fr beta, - Fr gamma, - uint256 offset - ) internal view returns (Fr publicInputDelta) { - Fr numerator = Fr.wrap(1); - Fr denominator = Fr.wrap(1); - - Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); - Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); - - { - for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { - Fr pubInput = FrLib.fromBytes32(publicInputs[i]); - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - Fr pubInput = pairingPointObject[i]; - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - } - - // Fr delta = numerator / denominator; // TOOO: batch invert later? - publicInputDelta = FrLib.div(numerator, denominator); - } - - function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { - Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 - Fr powPartialEvaluation = Fr.wrap(1); - - // We perform sumcheck reductions over log n rounds ( the multivariate degree ) - for (uint256 round; round < $LOG_N; ++round) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; - Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; - require(totalSum == roundTargetSum, Errors.SumcheckFailed()); - - Fr roundChallenge = tp.sumCheckUChallenges[round]; - - // Update the round target for the next rounf - roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); - powPartialEvaluation = - powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); - } - - // Last round - // For ZK flavors: sumcheckEvaluations has 42 elements - // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations - Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; - for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { - relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 - } - Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( - relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation - ); - - Fr evaluation = Fr.wrap(1); - for (uint256 i = 2; i < $LOG_N; i++) { - evaluation = evaluation * tp.sumCheckUChallenges[i]; - } - - grandHonkRelationSum = - grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; - verified = (grandHonkRelationSum == roundTargetSum); - } - - // Return the new target sum for the next sumcheck round - function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) - internal - view - returns (Fr targetSum) - { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) - ]; - - // To compute the next target sum, we evaluate the given univariate at a point u (challenge). - - // Performing Barycentric evaluations - // Compute B(x) - Fr numeratorValue = Fr.wrap(1); - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); - } - - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); - } - - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; - } - - // Scale the sum by the value of B(x) - targetSum = targetSum * numeratorValue; - } - - function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) - internal - view - returns (bool verified) - { - CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack - - // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size - Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); - // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings - Fr[] memory scalars = new Fr[]($MSMSize); - Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); - - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); - - mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); - mem.shiftedScalar = - tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); - - scalars[0] = Fr.wrap(1); - commitments[0] = proof.shplonkQ; - - /* Batch multivariate opening claims, shifted and unshifted - * The vector of scalars is populated as follows: - * \f[ - * \left( - * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) - * \right) - * \f] - * - * The following vector is concatenated to the vector of commitments: - * \f[ - * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} - * \f] - * - * Simultaneously, the evaluation of the multilinear polynomial - * \f[ - * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} - * \f] - * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. - * - * This approach minimizes the number of iterations over the commitments to multilinear polynomials - * and eliminates the need to store the powers of \f$ \rho \f$. - */ - // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] - // Start batching challenge at 1, not rho, to match non-ZK pattern - mem.batchingChallenge = Fr.wrap(1); - mem.batchedEvaluation = Fr.wrap(0); - - mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); - mem.shiftedScalarNeg = mem.shiftedScalar.neg(); - - // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) - for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { - scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; - mem.batchedEvaluation = mem.batchedEvaluation - + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - // g commitments are accumulated at r - // For each of the to be shifted commitments perform the shift in place by - // adding to the unshifted value. - // We do so, as the values are to be used in batchMul later, and as - // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. - // Applied to w1, w2, w3, w4 and zPerm - for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { - uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; - uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; - - scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); - mem.batchedEvaluation = - mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - - commitments[1] = proof.geminiMaskingPoly; - - commitments[2] = vk.qm; - commitments[3] = vk.qc; - commitments[4] = vk.ql; - commitments[5] = vk.qr; - commitments[6] = vk.qo; - commitments[7] = vk.q4; - commitments[8] = vk.qLookup; - commitments[9] = vk.qArith; - commitments[10] = vk.qDeltaRange; - commitments[11] = vk.qElliptic; - commitments[12] = vk.qMemory; - commitments[13] = vk.qNnf; - commitments[14] = vk.qPoseidon2External; - commitments[15] = vk.qPoseidon2Internal; - commitments[16] = vk.s1; - commitments[17] = vk.s2; - commitments[18] = vk.s3; - commitments[19] = vk.s4; - commitments[20] = vk.id1; - commitments[21] = vk.id2; - commitments[22] = vk.id3; - commitments[23] = vk.id4; - commitments[24] = vk.t1; - commitments[25] = vk.t2; - commitments[26] = vk.t3; - commitments[27] = vk.t4; - commitments[28] = vk.lagrangeFirst; - commitments[29] = vk.lagrangeLast; - - // Accumulate proof points - commitments[30] = proof.w1; - commitments[31] = proof.w2; - commitments[32] = proof.w3; - commitments[33] = proof.w4; - commitments[34] = proof.zPerm; - commitments[35] = proof.lookupInverses; - commitments[36] = proof.lookupReadCounts; - commitments[37] = proof.lookupReadTags; - - /* Batch gemini claims from the prover - * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from - * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars - * - * 1. Moves the vector - * \f[ - * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) - * \f] - * to the 'commitments' vector. - * - * 2. Computes the scalars: - * \f[ - * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} - * \f] - * and places them into the 'scalars' vector. - * - * 3. Accumulates the summands of the constant term: - * \f[ - * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} - * \f] - * and adds them to the 'constant_term_accumulator'. - */ - - // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 - Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( - tp.sumCheckUChallenges, - mem.batchedEvaluation, - proof.geminiAEvaluations, - powers_of_evaluation_challenge, - $LOG_N - ); - - mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; - mem.constantTermAccumulator = - mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); - - mem.batchingChallenge = tp.shplonkNu.sqr(); - uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; - - // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; - // Compute scalar multipliers for each fold commitment - for (uint256 i = 0; i < $LOG_N - 1; ++i) { - bool dummy_round = i >= ($LOG_N - 1); - - if (!dummy_round) { - // Update inverted denominators - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); - - // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] - mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; - mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; - scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); - - // Accumulate the const term contribution given by - // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) - Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; - accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; - mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; - } - // Update the running power of v - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; - - commitments[boundary + i] = proof.geminiFoldComms[i]; - } - - boundary += $LOG_N - 1; - - // Finalize the batch opening claim - mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); - mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); - mem.denominators[2] = mem.denominators[0]; - mem.denominators[3] = mem.denominators[0]; - - for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; - } - scalars[boundary] = mem.batchingScalars[0]; - scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; - scalars[boundary + 2] = mem.batchingScalars[3]; - - for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { - commitments[boundary++] = proof.libraCommitments[i]; - } - - commitments[boundary] = Honk.G1Point({x: 1, y: 2}); - scalars[boundary++] = mem.constantTermAccumulator; - - require( - checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), - Errors.ConsistencyCheckFailed() - ); - - Honk.G1Point memory quotient_commitment = proof.kzgQuotient; - - commitments[boundary] = quotient_commitment; - scalars[boundary] = tp.shplonkZ; // evaluation challenge - - PairingInputs memory pair; - pair.P_0 = batchMul(commitments, scalars); - pair.P_1 = negateInplace(quotient_commitment); - - // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) - if (!arePairingPointsDefault(proof.pairingPointObject)) { - Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); - (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = - convertPairingPointsToG1(proof.pairingPointObject); - - // Validate the points from the proof are on the curve - rejectPointAtInfinity(P_0_other); - rejectPointAtInfinity(P_1_other); - - // accumulate with aggregate points in proof - pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); - pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); - } - - return pairing(pair.P_0, pair.P_1); - } - - function checkEvalsConsistency( - Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, - Fr geminiR, - Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, - Fr libraEval - ) internal view returns (bool check) { - Fr one = Fr.wrap(1); - Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; - require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); - - SmallSubgroupIpaIntermediates memory mem; - mem.challengePolyLagrange[0] = one; - for (uint256 round = 0; round < $LOG_N; round++) { - uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; - mem.challengePolyLagrange[currIdx] = one; - for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { - mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; - } - } - - mem.rootPower = one; - mem.challengePolyEval = Fr.wrap(0); - for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { - mem.denominators[idx] = mem.rootPower * geminiR - one; - mem.denominators[idx] = mem.denominators[idx].invert(); - mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; - mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; - } - - Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); - mem.challengePolyEval = mem.challengePolyEval * numerator; - mem.lagrangeFirst = mem.denominators[0] * numerator; - mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; - - mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; - - mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) - * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); - mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; - - check = mem.diff == Fr.wrap(0); - } - - // This implementation is the same as above with different constants - function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) - internal - view - returns (Honk.G1Point memory result) - { - uint256 limit = $MSMSize; - - // Validate all points are on the curve - for (uint256 i = 0; i < limit; ++i) { - rejectPointAtInfinity(base[i]); - } - - bool success = true; - assembly ("memory-safe") { - let free := mload(0x40) - - let count := 0x01 - for {} lt(count, add(limit, 1)) { count := add(count, 1) } { - // Get loop offsets - let base_base := add(base, mul(count, 0x20)) - let scalar_base := add(scalars, mul(count, 0x20)) - - mstore(add(free, 0x40), mload(mload(base_base))) - mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) - // Add scalar - mstore(add(free, 0x80), mload(scalar_base)) - - success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) - // accumulator = accumulator + accumulator_2 - success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) - } - - // Return the result - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - } - - require(success, Errors.ShpleminiFailed()); - } - - // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) - function calculateProofSize(uint256 logN) internal pure returns (uint256) { - // Witness and Libra commitments - uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments - proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking - - // Sumcheck - proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates - proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations - - // Libra and Gemini - proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval - proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations - proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations - - // PCS commitments - proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments - proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments - - // Pairing points - proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs - - return proofLength * 32; - } - - function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); -} - -contract HonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { - function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { - return HonkVerificationKey.loadVerificationKey(); - } -} diff --git a/solidity/archived/login/oidc/script/DeployOidc.s.sol b/solidity/archived/login/oidc/script/DeployOidc.s.sol deleted file mode 100644 index 75680120..00000000 --- a/solidity/archived/login/oidc/script/DeployOidc.s.sol +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Script, console} from "forge-std/Script.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {GoogleOidcVerifier, IVerifier} from "../GoogleOidcVerifier.sol"; -import {HonkVerifier} from "../Verifier.sol"; - -interface IRegistrySetOidc { - function setOidcVerifier(string calldata platform, address verifier) external; -} - -/** - * Deploy the OIDC verifier stack (HonkVerifier + GoogleOidcVerifier) and - * wire `Registry.setOidcVerifier("www.googleapis.com", …)`. - * - * Runs under the `oidc` Foundry profile (via_ir=false). - * - * Usage: - * FOUNDRY_PROFILE=oidc forge script contracts/login/oidc/script/DeployOidc.s.sol \ - * --rpc-url $RPC_URL --broadcast \ - * --sig "run(address,address)" $REGISTRY $NOTARY_CONTRACT_ADDR - * - * The second argument is the shared Notary CONTRACT (the proxy every - * consumer points at), not a raw signer key. - */ -contract DeployOidc is Script { - function run(address registry, address notaryContract) external { - // DEPLOYER_KEY may arrive with or without a `0x` prefix (dev-start - // stores it stripped in .env; --private-key injects 64 bare hex - // chars). Normalize before parsing so `vm.parseUint` accepts it. - string memory deployerKeyHex = vm.envOr("DEPLOYER_KEY", vm.envOr("PRIVATE_KEY", string(""))); - require(bytes(deployerKeyHex).length > 0, "set DEPLOYER_KEY or PRIVATE_KEY"); - if (bytes(deployerKeyHex).length == 64) { - deployerKeyHex = string.concat("0x", deployerKeyHex); - } - uint256 deployerKey = vm.parseUint(deployerKeyHex); - address deployer = vm.addr(deployerKey); - // Google OAuth client id (the JWT `aud`) this verifier accepts. The - // circuit is application-agnostic, so the deployment is what pins - // registrations to one app — required, no silent default. Changeable - // later via GoogleOidcVerifier.setExpectedAudience(clientId). - string memory gmailClientId = vm.envString("GMAIL_CLIENT_ID"); - require(bytes(gmailClientId).length > 0, "set GMAIL_CLIENT_ID (the accepted Google OAuth client id)"); - console.log("Deployer: ", deployer); - console.log("Registry: ", registry); - console.log("Notary contract:", notaryContract); - console.log("Gmail client id:", gmailClientId); - - vm.startBroadcast(deployerKey); - - HonkVerifier honk = new HonkVerifier(); - // UUPS: deploy implementation, then an ERC1967 proxy initialized with it. - GoogleOidcVerifier googleImpl = new GoogleOidcVerifier(); - ERC1967Proxy googleProxy = new ERC1967Proxy( - address(googleImpl), - abi.encodeCall( - GoogleOidcVerifier.initialize, (IVerifier(address(honk)), deployer, notaryContract, gmailClientId) - ) - ); - GoogleOidcVerifier google = GoogleOidcVerifier(address(googleProxy)); - - IRegistrySetOidc(registry).setOidcVerifier("www.googleapis.com", address(google)); - - vm.stopBroadcast(); - - console.log("---"); - console.log("HONK_VERIFIER_ADDRESS=", address(honk)); - console.log("GOOGLE_OIDC_VERIFIER_ADDRESS=", address(google)); - } -} diff --git a/solidity/archived/login/test/Registry.t.sol b/solidity/archived/login/test/Registry.t.sol deleted file mode 100644 index 5c44ce88..00000000 --- a/solidity/archived/login/test/Registry.t.sol +++ /dev/null @@ -1,1068 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../Registry.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {WalletFactory} from "../WalletFactory.sol"; -import {WebWallet} from "../WebWallet.sol"; - -contract RegistryTest is Test { - Registry registry; - WalletFactory factory; - Notary notaryContract; - WebWallet walletImplV1; - - address owner = address(this); - - uint256 notaryPk = 0xA001; - uint256 backendPk = 0xA002; - uint256 userPk = 0xA003; - uint256 linkSessionPk = 0xA009; - - address notaryAddr; - address backendAddr; - - address userSessionAddr; - address linkSessionAddr; - - // ─── setUp ────────────────────────────────────────────────────── - - function setUp() public { - notaryAddr = vm.addr(notaryPk); - backendAddr = vm.addr(backendPk); - userSessionAddr = vm.addr(userPk); - linkSessionAddr = vm.addr(linkSessionPk); - - walletImplV1 = new WebWallet(); - - Registry rImpl = new Registry(); - WalletFactory fImpl = new WalletFactory(); - - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (owner, address(walletImplV1), address(1))) - ) - ) - ); - - notaryContract = deployNotary(owner, notaryAddr); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall(Registry.initialize, (address(notaryContract), backendAddr, address(factory), owner)) - ) - ) - ); - - factory.setRegistry(address(registry)); - - // Register platforms used in tests - registry.setPlatform("x.com", "api.x.com/2/users/me", '"username":"', '"id":"', '"'); - registry.setPlatform("github.com", "api.github.com/user", '"login":"', '"id":', ","); - - vm.warp(1_000_000); - } - - // ─── proof helpers ────────────────────────────────────────────── - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _buildPaths(bytes memory domainBytes, bytes memory usernameBytes, bytes memory endpointBytes) - internal - pure - returns ( - bytes32 transcriptRoot, - bytes32[] memory domainPath, - bytes32[] memory usernamePath, - bytes32[] memory endpointPath - ) - { - bytes32 leafD = _leaf("domain:", domainBytes); - bytes32 leafU = _leaf("recv:", usernameBytes); - bytes32 leafE = _leaf("endpoint:", endpointBytes); - - bytes32 hDu = - leafD < leafU ? keccak256(abi.encodePacked(leafD, leafU)) : keccak256(abi.encodePacked(leafU, leafD)); - - transcriptRoot = hDu < leafE ? keccak256(abi.encodePacked(hDu, leafE)) : keccak256(abi.encodePacked(leafE, hDu)); - - domainPath = new bytes32[](2); - domainPath[0] = leafU; - domainPath[1] = leafE; - - usernamePath = new bytes32[](2); - usernamePath[0] = leafD; - usernamePath[1] = leafE; - - endpointPath = new bytes32[](1); - endpointPath[0] = hDu; - } - - struct ProofBundle { - Registry.FullTlsProof proof; - } - - function _buildProof( - string memory domain, - string memory username, - string memory endpoint, - string memory handlePrefix, - address _userAddress, - uint256 ts - ) internal view returns (ProofBundle memory b) { - return _buildProofForWallet(domain, username, endpoint, handlePrefix, _userAddress, address(0), ts); - } - - function _buildProofForWallet( - string memory domain, - string memory username, - string memory endpoint, - string memory handlePrefix, - address _userAddress, - address _walletAddress, - uint256 ts - ) internal view returns (ProofBundle memory b) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes memory usernameSnippet = abi.encodePacked(handlePrefix, username, '"'); - (bytes32 transcriptRoot, bytes32[] memory dp, bytes32[] memory up, bytes32[] memory ep) = - _buildPaths(bytes(domain), usernameSnippet, bytes(endpoint)); - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(_userAddress, _walletAddress, transcriptRoot, ts)); - - b.proof = Registry.FullTlsProof({ - notarySignature: _sign(0xA001, notaryDigest), - backendSignature: _sign(0xA002, backendDigest), - userAddress: _userAddress, - walletAddress: _walletAddress, - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: new bytes32[](0) - }); - } - - function _buildXProof(uint256 ts) internal view returns (ProofBundle memory) { - return _buildProofWithId( - "x.com", "alice", "12345", "api.x.com/2/users/me", '"username":"', '"id":"', '"', userSessionAddr, ts - ); - } - - function _registerXAlice() internal returns (address wallet) { - ProofBundle memory b = _buildXProof(block.timestamp); - registry.register_session(b.proof, "x.com", "alice", "12345", "api.x.com/2/users/me"); - wallet = registry.resolve("x.com", "alice"); - } - - // 4-leaf tree: D, U, E, Id. hDU = h(D,U), hEId = h(E,Id), root = h(hDU, hEId). - function _buildProofWithId( - string memory domain, - string memory username, - string memory userId, - string memory endpoint, - string memory handlePrefix, - string memory idPrefix, - string memory idSuffix, - address _userAddress, - uint256 ts - ) internal view returns (ProofBundle memory b) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes32 leafD = _leaf("domain:", bytes(domain)); - bytes32 leafU = _leaf("recv:", abi.encodePacked(handlePrefix, username, '"')); - bytes32 leafE = _leaf("endpoint:", bytes(endpoint)); - bytes32 leafId = _leaf("recv:", abi.encodePacked(idPrefix, userId, idSuffix)); - - bytes32 hDU = _hash(leafD, leafU); - bytes32 hEId = _hash(leafE, leafId); - bytes32 root = _hash(hDU, hEId); - - bytes32[] memory dp = new bytes32[](2); - dp[0] = leafU; - dp[1] = hEId; - bytes32[] memory up = new bytes32[](2); - up[0] = leafD; - up[1] = hEId; - bytes32[] memory ep = new bytes32[](2); - ep[0] = leafId; - ep[1] = hDU; - bytes32[] memory idp = new bytes32[](2); - idp[0] = leafE; - idp[1] = hDU; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - root, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(_userAddress, address(0), root, ts)); - - b.proof = Registry.FullTlsProof({ - notarySignature: _sign(0xA001, notaryDigest), - backendSignature: _sign(0xA002, backendDigest), - userAddress: _userAddress, - walletAddress: address(0), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: root, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: idp - }); - } - - /// Like `_buildProofWithId` but bound to a linking wallet: `linkIdentity` - /// requires `proof.walletAddress == msg.sender`, so re-bind walletAddress - /// and re-sign the backend digest (notary digest is wallet-independent). - function _linkProofWithId( - string memory domain, - string memory username, - string memory userId, - string memory endpoint, - string memory handlePrefix, - string memory idPrefix, - string memory idSuffix, - address _userAddress, - address _walletAddress, - uint256 ts - ) internal view returns (ProofBundle memory b) { - b = _buildProofWithId(domain, username, userId, endpoint, handlePrefix, idPrefix, idSuffix, _userAddress, ts); - b.proof.walletAddress = _walletAddress; - b.proof.backendSignature = - _sign(0xA002, keccak256(abi.encode(_userAddress, _walletAddress, b.proof.transcriptRoot, ts))); - } - - function _hash(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - // Execute `linkData` on `wallet` via the multisig executor, signed by userPk. - function _execLink(address wallet, bytes memory linkData) internal { - uint256 currentNonce = WebWallet(payable(wallet)).nonce(); - bytes32 execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(linkData), currentNonce, block.chainid, wallet) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(userPk, execDigest); - WebWallet(payable(wallet)).execute(address(registry), 0, linkData, sigs); - } - - // ─── happy path: new user ─────────────────────────────────────── - - function test_registerSession_newUser_deploysWalletAndRegistersSession() public { - address wallet = _registerXAlice(); - - assertTrue(wallet != address(0)); - assertTrue(factory.isRegisteredWallet(wallet)); - assertEq(registry.resolve("x.com", "alice"), wallet); - - WebWallet w = WebWallet(payable(wallet)); - bytes32 key = keccak256(abi.encode("x.com", "12345")); - assertEq(w.sessionCount(key), 1); - assertEq(w.sessionAt(key, 0), userSessionAddr); - - assertTrue(w.isIdentity(key)); - } - - // ─── happy path: existing user ────────────────────────────────── - - function test_registerSession_existingUser_registersNewSession() public { - address wallet = _registerXAlice(); - - uint256 user2Pk = 0xA004; - address user2Addr = vm.addr(user2Pk); - - ProofBundle memory b = _buildProofWithId( - "x.com", "alice", "12345", "api.x.com/2/users/me", '"username":"', '"id":"', '"', user2Addr, block.timestamp - ); - - registry.register_session(b.proof, "x.com", "alice", "12345", "api.x.com/2/users/me"); - - assertEq(registry.resolve("x.com", "alice"), wallet); - - WebWallet w = WebWallet(payable(wallet)); - bytes32 key = keccak256(abi.encode("x.com", "12345")); - assertEq(w.sessionCount(key), 2); - assertEq(w.sessionAt(key, 1), user2Addr); - } - - // ─── invalid proof rejected ───────────────────────────────────── - - function test_registerSession_invalidNotarySignature_reverts() public { - ProofBundle memory b = _buildXProof(block.timestamp); - - // Re-sign notary digest with wrong key - bytes32 notaryDigest = keccak256( - abi.encode( - b.proof.domainHash, - b.proof.clientRandom, - b.proof.serverRandom, - keccak256(b.proof.serverEphemeralKey), - b.proof.transcriptRoot, - b.proof.timestamp - ) - ); - b.proof.notarySignature = _sign(0xDEAD, notaryDigest); - - vm.expectRevert(); - registry.register_session(b.proof, "x.com", "alice", "", "api.x.com/2/users/me"); - } - - function test_registerSession_invalidBackendSignature_reverts() public { - ProofBundle memory b = _buildXProof(block.timestamp); - - bytes32 backendDigest = keccak256(abi.encode(b.proof.userAddress, b.proof.transcriptRoot, b.proof.timestamp)); - b.proof.backendSignature = _sign(notaryPk, backendDigest); - - vm.expectRevert(Registry.InvalidBackendSignature.selector); - registry.register_session(b.proof, "x.com", "alice", "", "api.x.com/2/users/me"); - } - - // ─── session address reuse rejected (flat — same addr any platform) ── - - function test_registerSession_sessionKeyReuse_reverts() public { - _registerXAlice(); - - ProofBundle memory b2 = _buildProofWithId( - "x.com", - "bob", - "67890", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - userSessionAddr, - block.timestamp - ); - - vm.expectRevert(Registry.SessionKeyAlreadyUsed.selector); - registry.register_session(b2.proof, "x.com", "bob", "67890", "api.x.com/2/users/me"); - } - - // ─── stale timestamp rejected ─────────────────────────────────── - - function test_registerSession_staleTimestamp_reverts() public { - ProofBundle memory b = _buildProof( - "x.com", "alice", "api.x.com/2/users/me", '"username":"', userSessionAddr, block.timestamp - 2 hours - ); - - vm.expectRevert(Registry.StaleProof.selector); - registry.register_session(b.proof, "x.com", "alice", "", "api.x.com/2/users/me"); - } - - function test_registerSession_futureTimestamp_reverts() public { - ProofBundle memory b = _buildProof( - "x.com", "alice", "api.x.com/2/users/me", '"username":"', userSessionAddr, block.timestamp + 5 minutes + 1 - ); - - vm.expectRevert(Registry.FutureProof.selector); - registry.register_session(b.proof, "x.com", "alice", "", "api.x.com/2/users/me"); - } - - // ─── WebWallet.register_session reverts if caller is not Registry ─ - - function test_webWalletRegisterSession_revertsIfNotRegistry() public { - address wallet = _registerXAlice(); - vm.expectRevert(WebWallet.OnlyRegistry.selector); - WebWallet(payable(wallet)).register_session("x.com", "alice", "12345", userSessionAddr); - } - - // ─── resolve returns address(0) for unknown handle ────────────── - - function test_resolve_returnsZeroForUnknownHandle() public view { - assertEq(registry.resolve("x.com", "nobody"), address(0)); - } - - // ─── getHandles ───────────────────────────────────────────────── - - function test_getHandles_returnsCorrectEntries() public { - _registerXAlice(); - (string[] memory platforms, string[] memory handles) = registry.getHandles(registry.resolve("x.com", "alice")); - assertEq(platforms.length, 1); - assertEq(platforms[0], "x.com"); - assertEq(handles[0], "alice"); - } - - // ─── linkIdentity tests ───────────────────────────────────────── - - function test_linkIdentity_happyPath() public { - address wallet = _registerXAlice(); - - ProofBundle memory ghProof = _linkProofWithId( - "github.com", - "alice", - "777", - "api.github.com/user", - '"login":"', - '"id":', - ",", - linkSessionAddr, - wallet, - block.timestamp - ); - - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("github.com", "alice", "777", ghProof.proof, "api.github.com/user")); - - uint256 currentNonce = WebWallet(payable(wallet)).nonce(); - bytes32 execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(linkData), currentNonce, block.chainid, wallet) - ); - - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(userPk, execDigest); - - WebWallet(payable(wallet)).execute(address(registry), 0, linkData, sigs); - - assertEq(registry.resolve("github.com", "alice"), wallet); - - (string[] memory platforms,) = registry.getHandles(wallet); - assertEq(platforms.length, 2); - } - - // Re-pointing a handle to a new owner is allowed; only an id mapped to a - // different wallet is a real conflict (see test_linkIdentity_idAlreadyLinked_reverts). - function test_linkIdentity_rePointHandle_allowed() public { - address aliceWallet = _registerXAlice(); - - // bob registers github handle "shared" under id 100. - uint256 bobPk = 0xA007; - address bobAddr = vm.addr(bobPk); - ProofBundle memory bobProof = _buildProofWithId( - "x.com", "shared", "100", "api.x.com/2/users/me", '"username":"', '"id":"', '"', bobAddr, block.timestamp - ); - registry.register_session(bobProof.proof, "x.com", "shared", "100", "api.x.com/2/users/me"); - - // alice links the same handle "shared" under a different id 200 — allowed. - ProofBundle memory ghProof = _linkProofWithId( - "x.com", - "shared", - "200", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - linkSessionAddr, - aliceWallet, - block.timestamp - ); - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("x.com", "shared", "200", ghProof.proof, "api.x.com/2/users/me")); - _execLink(aliceWallet, linkData); - - // handleHint re-points to alice's id; resolve follows. - assertEq(registry.resolveById("x.com", "200"), aliceWallet); - assertEq(registry.resolve("x.com", "shared"), aliceWallet); - } - - /// M2 regression: re-linking the SAME (platform, userId) to the SAME wallet - /// must not append a duplicate HandleEntry (which would double-count - /// that id downstream). The session/label still refresh. - function test_linkIdentity_reLinkSameId_noDuplicateEntry() public { - address wallet = _registerXAlice(); // x.com / alice / id 12345 - - (string[] memory before,) = registry.getUserIds(wallet); - assertEq(before.length, 1, "precondition: one entry"); - - // Re-link the SAME id "12345" to the same wallet with a fresh session key. - address freshSession = vm.addr(0xFEED01); - ProofBundle memory p = _linkProofWithId( - "x.com", - "alice", - "12345", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - freshSession, - wallet, - block.timestamp - ); - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("x.com", "alice", "12345", p.proof, "api.x.com/2/users/me")); - _execLink(wallet, linkData); - - // No duplicate entry — still exactly one (platform, id) for this wallet. - (string[] memory platforms, string[] memory userIds) = registry.getUserIds(wallet); - assertEq(platforms.length, 1, "duplicate HandleEntry appended on re-link"); - assertEq(userIds[0], "12345"); - // Still resolves correctly; the fresh session is now authorized. - assertEq(registry.resolveById("x.com", "12345"), wallet); - assertTrue(WebWallet(payable(wallet)).isSession(freshSession)); - } - - function test_linkIdentity_notRegisteredWallet_reverts() public { - ProofBundle memory ghProof = - _buildProof("github.com", "alice", "api.github.com/user", '"login":"', userSessionAddr, block.timestamp); - - vm.expectRevert(Registry.NotRegisteredWallet.selector); - registry.linkIdentity("github.com", "alice", "", ghProof.proof, "api.github.com/user"); - } - - // ─── Multisig threshold with (platform, handle) pairs ─────────── - - function test_execute_multisigThreshold_distinctPairsRequired() public { - address wallet = _registerXAlice(); - - // Register a second session for same (x.com, alice) - uint256 user2Pk = 0xA006; - address user2Addr = vm.addr(user2Pk); - - ProofBundle memory b2 = _buildProofWithId( - "x.com", "alice", "12345", "api.x.com/2/users/me", '"username":"', '"id":"', '"', user2Addr, block.timestamp - ); - registry.register_session(b2.proof, "x.com", "alice", "12345", "api.x.com/2/users/me"); - - // Link a second identity (use a fresh session address for the new identity) - ProofBundle memory ghProof = _linkProofWithId( - "github.com", - "alice", - "777", - "api.github.com/user", - '"login":"', - '"id":', - ",", - linkSessionAddr, - wallet, - block.timestamp - ); - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("github.com", "alice", "777", ghProof.proof, "api.github.com/user")); - uint256 currentNonce = WebWallet(payable(wallet)).nonce(); - bytes32 execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(linkData), currentNonce, block.chainid, wallet) - ); - bytes[] memory sg1 = new bytes[](1); - sg1[0] = _sign(userPk, execDigest); - WebWallet(payable(wallet)).execute(address(registry), 0, linkData, sg1); - - // Register a session for github.com/alice - uint256 ghSessionPk = 0xA008; - address ghAddr = vm.addr(ghSessionPk); - - ProofBundle memory ghSessionProof = _buildProofWithId( - "github.com", "alice", "777", "api.github.com/user", '"login":"', '"id":', ",", ghAddr, block.timestamp - ); - registry.register_session(ghSessionProof.proof, "github.com", "alice", "777", "api.github.com/user"); - - // Execute with 2 sigs from same pair (x.com/alice) — counts as 1 distinct pair - bytes memory noopData = abi.encodeWithSignature("resolve(string,string)", "x", "y"); - currentNonce = WebWallet(payable(wallet)).nonce(); - execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(noopData), currentNonce, block.chainid, wallet) - ); - - bytes[] memory sg3 = new bytes[](2); - sg3[0] = _sign(userPk, execDigest); - sg3[1] = _sign(user2Pk, execDigest); - - // threshold=1, 2 sigs from 1 pair → 1 distinct pair >= 1 ✓ - WebWallet(payable(wallet)).execute(address(registry), 0, noopData, sg3); - } - - // ─── linkIdentity registers session on wallet ─────────────────── - - function test_linkIdentity_registersSessionOnWallet() public { - address wallet = _registerXAlice(); - - ProofBundle memory ghProof = _linkProofWithId( - "github.com", - "alice", - "777", - "api.github.com/user", - '"login":"', - '"id":', - ",", - linkSessionAddr, - wallet, - block.timestamp - ); - - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("github.com", "alice", "777", ghProof.proof, "api.github.com/user")); - - uint256 currentNonce = WebWallet(payable(wallet)).nonce(); - bytes32 execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(linkData), currentNonce, block.chainid, wallet) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(userPk, execDigest); - WebWallet(payable(wallet)).execute(address(registry), 0, linkData, sigs); - - // Verify that linkSessionAddr is now a session key on the wallet - bytes32 ghKey = keccak256(abi.encode("github.com", "777")); - WebWallet w = WebWallet(payable(wallet)); - assertTrue(w.isSession(linkSessionAddr)); - assertEq(w.sessionCount(ghKey), 1); - assertEq(w.sessionAt(ghKey, 0), linkSessionAddr); - } - - // ─── getHandles returns empty for unknown wallet ──────────────── - - function test_getHandles_returnsEmptyForUnknownWallet() public view { - (string[] memory platforms, string[] memory handles) = registry.getHandles(address(0xDEAD)); - assertEq(platforms.length, 0); - assertEq(handles.length, 0); - } - - // ─── register_session zero address ────────────────────────────── - - function test_registerSession_zeroAddress_reverts() public { - ProofBundle memory b = _buildProofWithId( - "x.com", - "alice", - "12345", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - address(0), - block.timestamp - ); - vm.expectRevert("zero session"); - registry.register_session(b.proof, "x.com", "alice", "12345", "api.x.com/2/users/me"); - } - - // ─── register_session domain hash mismatch ────────────────────── - - function test_registerSession_unsupportedPlatform_reverts() public { - ProofBundle memory b = _buildXProof(block.timestamp); - // Pass unregistered domain - vm.expectRevert(Registry.UnsupportedPlatform.selector); - registry.register_session(b.proof, "wrong.com", "alice", "", "api.x.com/2/users/me"); - } - - // ─── platform userId ──────────────────────────────────────────── - - function test_registerSession_storesUserId() public { - ProofBundle memory b = _buildProofWithId( - "x.com", - "alice", - "12345", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - userSessionAddr, - block.timestamp - ); - registry.register_session(b.proof, "x.com", "alice", "12345", "api.x.com/2/users/me"); - - address wallet = registry.walletOf("x.com", "12345"); - (string[] memory platforms, string[] memory userIds) = registry.getUserIds(wallet); - assertEq(platforms.length, 1); - assertEq(platforms[0], "x.com"); - assertEq(userIds[0], "12345"); - } - - function test_registerSession_wrongUserId_reverts() public { - // Proof commits to id 12345, but caller claims 99999 → id leaf mismatch. - ProofBundle memory b = _buildProofWithId( - "x.com", - "alice", - "12345", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - userSessionAddr, - block.timestamp - ); - vm.expectRevert(Registry.InvalidMerkleProof.selector); - registry.register_session(b.proof, "x.com", "alice", "99999", "api.x.com/2/users/me"); - } - - function test_registerSession_idOnUnsupportedPlatform_reverts() public { - // A platform with empty idPrefix → passing a userId must revert. - registry.setPlatform("noid.com", "api.noid.com/me", '"username":"', "", ""); - ProofBundle memory b = _buildProofWithId( - "noid.com", - "alice", - "999", - "api.noid.com/me", - '"username":"', - '"id":"', - '"', - userSessionAddr, - block.timestamp - ); - vm.expectRevert("id not supported"); - registry.register_session(b.proof, "noid.com", "alice", "999", "api.noid.com/me"); - } - - // GitHub: bare-number id "id":, (suffix ',') — stores the numeric id. - function test_registerSession_githubNumericId_stored() public { - ProofBundle memory b = _buildProofWithId( - "github.com", - "octocat", - "583231", - "api.github.com/user", - '"login":"', - '"id":', - ",", - userSessionAddr, - block.timestamp - ); - registry.register_session(b.proof, "github.com", "octocat", "583231", "api.github.com/user"); - - address wallet = registry.walletOf("github.com", "583231"); - assertTrue(wallet != address(0)); - assertEq(registry.resolveById("github.com", "583231"), wallet); - (, string[] memory userIds) = registry.getUserIds(wallet); - assertEq(userIds[0], "583231"); - } - - // GitHub: a truncated id (claim "583" against real "583231,") must not verify — - // the ',' suffix anchors the number's end, blocking prefix-squatting. - function test_registerSession_githubTruncatedId_reverts() public { - ProofBundle memory b = _buildProofWithId( - "github.com", - "octocat", - "583231", - "api.github.com/user", - '"login":"', - '"id":', - ",", - userSessionAddr, - block.timestamp - ); - vm.expectRevert(Registry.InvalidMerkleProof.selector); - registry.register_session(b.proof, "github.com", "octocat", "583", "api.github.com/user"); - } - - // ─── id-layer: registration / resolution (T1–T12) ─────────────── - - // Register x.com/alice under id 12345 via the id path. - function _registerXAliceId(string memory userId, address sessionAddr, uint256 ts) - internal - returns (address wallet) - { - ProofBundle memory b = _buildProofWithId( - "x.com", "alice", userId, "api.x.com/2/users/me", '"username":"', '"id":"', '"', sessionAddr, ts - ); - registry.register_session(b.proof, "x.com", "alice", userId, "api.x.com/2/users/me"); - wallet = registry.walletOf("x.com", userId); - } - - // T1: register by id deploys wallet, sets walletOf/handleHint/idMeta. - function test_T1_registerById_setsIdState() public { - address wallet = _registerXAliceId("12345", userSessionAddr, block.timestamp); - - assertTrue(wallet != address(0)); // I1: walletOf set to a non-zero wallet - assertEq(registry.walletOf("x.com", "12345"), wallet); - assertEq(registry.handleHint("x.com", "alice"), "12345"); - (string memory h, uint256 verifiedAt) = registry.idMeta("x.com", "12345"); - assertEq(h, "alice"); - assertEq(verifiedAt, block.timestamp); - } - - // T2: resolve(handle) follows handleHint → walletOf. - function test_T2_resolve_viaHint() public { - address wallet = _registerXAliceId("12345", userSessionAddr, block.timestamp); - assertEq(registry.resolve("x.com", "alice"), wallet); - } - - // T3: resolveById(id) → wallet. - function test_T3_resolveById() public { - address wallet = _registerXAliceId("12345", userSessionAddr, block.timestamp); - assertEq(registry.resolveById("x.com", "12345"), wallet); - } - - // T4: recycle — B proves a handle A held; resolve→B, resolveById(A)→A's wallet. - function test_T4_recycle_handlePointsToNewOwner() public { - address walletA = _registerXAliceId("100", userSessionAddr, block.timestamp); - - // B proves the same handle "alice" under a fresh id 200. - uint256 bPk = 0xB001; - address bAddr = vm.addr(bPk); - address walletB = _registerXAliceId("200", bAddr, block.timestamp); - - assertTrue(walletA != walletB); - // I2/I3: hint re-points to B; A still reachable by id. - assertEq(registry.resolve("x.com", "alice"), walletB); - assertEq(registry.resolveById("x.com", "100"), walletA); - assertEq(registry.resolveById("x.com", "200"), walletB); - // A's idMeta untouched. - (string memory hA,) = registry.idMeta("x.com", "100"); - assertEq(hA, "alice"); - } - - // Rename: id 100 re-registers under a new handle. The OLD handle's hint must - // be cleared so resolve(old) no longer returns this wallet. - function test_rename_clearsStaleHandleHint() public { - address wallet = _registerXAliceId("100", userSessionAddr, block.timestamp); - assertEq(registry.resolve("x.com", "alice"), wallet); - - // Same id 100 now proves handle "alice2" with a fresh session key. - address sess2 = vm.addr(0xA11CE2); - ProofBundle memory b = _buildProofWithId( - "x.com", "alice2", "100", "api.x.com/2/users/me", '"username":"', '"id":"', '"', sess2, block.timestamp - ); - vm.expectEmit(false, false, false, true, address(registry)); - emit Registry.HandleChanged("x.com", "100", "alice2"); - registry.register_session(b.proof, "x.com", "alice2", "100", "api.x.com/2/users/me"); - - assertEq(registry.resolve("x.com", "alice2"), wallet); - assertEq(registry.resolveById("x.com", "100"), wallet); - // Old handle hint cleared → no longer resolves to the wallet. - assertEq(registry.resolve("x.com", "alice"), address(0)); - assertEq(registry.handleHint("x.com", "alice"), ""); - // getHandles stays frozen (claim-key stable); getCurrentHandles is current. - (, string[] memory frozenHs) = registry.getHandles(wallet); - assertEq(frozenHs[0], "alice"); - (, string[] memory curHs) = registry.getCurrentHandles(wallet); - assertEq(curHs.length, 1); - assertEq(curHs[0], "alice2"); - } - - // T5: linkIdentity binds a new id to an existing wallet. - function test_T5_linkIdentity_newId() public { - address wallet = _registerXAliceId("100", userSessionAddr, block.timestamp); - - ProofBundle memory ghProof = _linkProofWithId( - "x.com", - "alice2", - "300", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - linkSessionAddr, - wallet, - block.timestamp - ); - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("x.com", "alice2", "300", ghProof.proof, "api.x.com/2/users/me")); - _execLink(wallet, linkData); - - assertEq(registry.walletOf("x.com", "300"), wallet); - assertEq(registry.resolveById("x.com", "300"), wallet); - } - - // T6: linkIdentity for an id already linked elsewhere → revert IdentityAlreadyLinked. - function test_T6_linkIdentity_idAlreadyLinked_reverts() public { - _registerXAliceId("100", userSessionAddr, block.timestamp); // wallet A owns id 100 - - // B's wallet attempts to link id 100 (already A's) → conflict. - uint256 bPk = 0xB002; - address bAddr = vm.addr(bPk); - address walletB = _registerXAliceId("200", bAddr, block.timestamp); - - ProofBundle memory p = _buildProofWithId( - "x.com", - "alice", - "100", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - linkSessionAddr, - block.timestamp - ); - bytes memory linkData = - abi.encodeCall(Registry.linkIdentity, ("x.com", "alice", "100", p.proof, "api.x.com/2/users/me")); - - uint256 currentNonce = WebWallet(payable(walletB)).nonce(); - bytes32 execDigest = keccak256( - abi.encode(address(registry), uint256(0), keccak256(linkData), currentNonce, block.chainid, walletB) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(bPk, execDigest); // bAddr is B's session - - vm.expectRevert(); // wrapped ExecutionFailed(IdentityAlreadyLinked) — I5 - WebWallet(payable(walletB)).execute(address(registry), 0, linkData, sigs); - } - - // T7: refreshHandleHint by a stranger with a fresh proof re-points the hint. - function test_T7_refreshHandleHint_permissionless() public { - address wallet = _registerXAliceId("100", userSessionAddr, block.timestamp); - - // Stranger calls refreshHandleHint with a fresh proof for (alice, 100). - ProofBundle memory p = _buildProofWithId( - "x.com", - "alice", - "100", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - address(0xCAFE), - block.timestamp - ); - vm.prank(address(0xBEEF)); - registry.refreshHandleHint("x.com", "100", "alice", p.proof, "api.x.com/2/users/me"); - - assertEq(registry.handleHint("x.com", "alice"), "100"); - assertEq(registry.resolve("x.com", "alice"), wallet); - } - - // T8: refreshHandleHint for an unknown id → revert. - function test_T8_refreshHandleHint_unknownId_reverts() public { - ProofBundle memory p = _buildProofWithId( - "x.com", - "alice", - "999", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - address(0xCAFE), - block.timestamp - ); - vm.expectRevert(Registry.UnknownId.selector); - registry.refreshHandleHint("x.com", "999", "alice", p.proof, "api.x.com/2/users/me"); - } - - // T9: refresh/register with a wrong id leaf → revert InvalidMerkleProof (I4). - function test_T9_wrongIdLeaf_reverts() public { - // Proof commits to id 100, caller claims 999 → id leaf mismatch. - ProofBundle memory p = _buildProofWithId( - "x.com", - "alice", - "100", - "api.x.com/2/users/me", - '"username":"', - '"id":"', - '"', - userSessionAddr, - block.timestamp - ); - vm.expectRevert(Registry.InvalidMerkleProof.selector); - registry.register_session(p.proof, "x.com", "alice", "999", "api.x.com/2/users/me"); - } - - // T10: github (bare-number id) registers + resolves via the id path. - function test_T10_githubId_resolves() public { - uint256 ghPk = 0xC001; - address ghAddr = vm.addr(ghPk); - ProofBundle memory b = _buildProofWithId( - "github.com", "octocat", "583231", "api.github.com/user", '"login":"', '"id":', ",", ghAddr, block.timestamp - ); - registry.register_session(b.proof, "github.com", "octocat", "583231", "api.github.com/user"); - - address wallet = registry.walletOf("github.com", "583231"); - assertTrue(wallet != address(0)); - assertEq(registry.resolve("github.com", "octocat"), wallet); - assertEq(registry.resolveById("github.com", "583231"), wallet); - } - - // T11: getUserIds returns (platform, id) after an id-register (2.6). - function test_T11_getUserIds_afterIdRegister() public { - address wallet = _registerXAliceId("12345", userSessionAddr, block.timestamp); - (string[] memory platforms, string[] memory userIds) = registry.getUserIds(wallet); - assertEq(platforms.length, 1); - assertEq(platforms[0], "x.com"); - assertEq(userIds[0], "12345"); - } - - // ─── ERC-7201 storage ─────────────────────────────────────────── - - /// The state must be under the ERC-7201 root and not in sequential slots. - /// This test reads the root with `vm.load` and does not use the constant in - /// the contract. The test fails if the constant is wrong. The test also - /// fails if a later edit adds a plain state variable. That variable lands - /// at slot 0. It collides with nothing today, but it breaks the append-only - /// guarantee that the namespace gives. - function test_storageLivesUnderTheErc7201Root() public view { - bytes32 root = keccak256(abi.encode(uint256(keccak256("dyaka.storage.Registry")) - 1)) & ~bytes32(uint256(0xff)); - - // `notaryContract` is the first field of RegistryStorage. The field is - // at the root. - assertTrue(registry.notaryContract() != address(0), "the fixture must set a notary contract"); - assertEq( - address(uint160(uint256(vm.load(address(registry), root)))), - registry.notaryContract(), - "notaryContract is not at the namespace root" - ); - - // No data is in the sequential slots that the contract used before. - for (uint256 i = 0; i < 10; i++) { - assertEq(vm.load(address(registry), bytes32(i)), bytes32(0), "a sequential slot is in use"); - } - } - - /// The getters keep the shape that the public variables generated. Every - /// off-chain binding (alloy `sol!`, viem ABI) continues to work after the - /// layout change. - function test_namespacedGettersKeepThePublicVariableAbi() public { - address wallet = _registerXAliceId("12345", userSessionAddr, block.timestamp); - - assertEq(registry.walletOf("x.com", "12345"), wallet); - (string memory handle, uint256 verifiedAt) = registry.idMeta("x.com", "12345"); - assertEq(handle, "alice"); - assertTrue(verifiedAt != 0); - assertEq(registry.handleHint("x.com", "alice"), "12345"); - - (string memory platform, string memory boundHandle, string memory userId) = - registry.contract_to_handles(wallet, 0); - assertEq(platform, "x.com"); - assertEq(boundHandle, "alice"); - assertEq(userId, "12345"); - - // All four fields. A previous review of this change caught a getter - // that returned only the first two and dropped `idPrefix` and - // `idSuffix`. The contract still compiled and the ABI still had the - // selector, so nothing failed until an id-bound proof was verified. - (string memory endpoint, string memory handlePrefix, string memory idPrefix, string memory idSuffix) = - registry.platformConfigs("x.com"); - assertEq(endpoint, "api.x.com/2/users/me"); - assertEq(handlePrefix, '"username":"'); - assertEq(idPrefix, '"id":"'); - assertEq(idSuffix, '"'); - } - - // ─── initialize double-init ───────────────────────────────────── - - function test_initialize_revertsOnDoubleInit() public { - vm.expectRevert(); - registry.initialize(address(notaryContract), backendAddr, address(factory), owner); - } -} diff --git a/solidity/archived/login/test/TlsLinkGriefPoC.t.sol b/solidity/archived/login/test/TlsLinkGriefPoC.t.sol deleted file mode 100644 index 63ff62a9..00000000 --- a/solidity/archived/login/test/TlsLinkGriefPoC.t.sol +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../Registry.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {WalletFactory} from "../WalletFactory.sol"; -import {WebWallet} from "../WebWallet.sol"; - -/// End-to-end PoC: walletAddress baked into the backend signature blocks -/// proof replay from a different `msg.sender`. Victim's proof is signed -/// with `walletAddress = W_v`; attacker tries to replay from W_atk → -/// contract requires `proof.walletAddress == msg.sender` → revert. -contract TlsLinkGriefPoC is Test { - Registry registry; - WalletFactory factory; - - uint256 constant NOTARY_KEY = 0xBEEF; - uint256 constant BACKEND_KEY = 0xBEED; - uint256 constant VICTIM_BOOT_KEY = 0xCA75; // signs victim wallet's executes - uint256 constant VICTIM_LINK_KEY = 0x713C71; // userAddress inside the leaked link proof - uint256 constant ATTACKER_BOOT_KEY = 0xA77ACE; // signs attacker wallet's executes - - address notaryAddr; - address backendAddr; - address victimBootAddr; - address victimLinkAddr; - address attackerBootAddr; - - string constant PLATFORM = "github.com"; - string constant ENDPOINT = "/user"; - string constant HANDLE_PREFIX = '"login":"'; - string constant VICTIM_TARGET = "victim"; - - function setUp() public { - notaryAddr = vm.addr(NOTARY_KEY); - backendAddr = vm.addr(BACKEND_KEY); - victimBootAddr = vm.addr(VICTIM_BOOT_KEY); - victimLinkAddr = vm.addr(VICTIM_LINK_KEY); - attackerBootAddr = vm.addr(ATTACKER_BOOT_KEY); - - WebWallet walletImpl = new WebWallet(); - WalletFactory fImpl = new WalletFactory(); - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(walletImpl), address(1))) - ) - ) - ); - Registry rImpl = new Registry(); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall( - Registry.initialize, - (address(deployNotary(address(this), notaryAddr)), backendAddr, address(factory), address(this)) - ) - ) - ) - ); - factory.setRegistry(address(registry)); - // GitHub-style bare-number id binding. - registry.setPlatform(PLATFORM, ENDPOINT, HANDLE_PREFIX, '"id":', ","); - } - - /// Deterministic numeric immutable id for a handle (identity keyed by id). - function _idFor(string memory handle) internal pure returns (string memory) { - return vm.toString(uint256(keccak256(bytes(handle))) % 1_000_000_000); - } - - function _sortedHash(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - function _buildTree(string memory handle, string memory userId) - internal - pure - returns ( - bytes32 root, - bytes32[] memory dPath, - bytes32[] memory uPath, - bytes32[] memory ePath, - bytes32[] memory idPath - ) - { - bytes32 dLeaf = keccak256(bytes.concat(keccak256(abi.encodePacked("domain:", bytes(PLATFORM))))); - bytes memory snippet = abi.encodePacked(bytes(HANDLE_PREFIX), bytes(handle), '"'); - bytes32 uLeaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", snippet)))); - bytes32 eLeaf = keccak256(bytes.concat(keccak256(abi.encodePacked("endpoint:", bytes(ENDPOINT))))); - // GitHub bare-number id leaf: "id":, - bytes memory idSnippet = abi.encodePacked('"id":', bytes(userId), ","); - bytes32 idLeaf = keccak256(bytes.concat(keccak256(abi.encodePacked("recv:", idSnippet)))); - - bytes32 p01 = _sortedHash(dLeaf, uLeaf); - bytes32 p23 = _sortedHash(eLeaf, idLeaf); - root = _sortedHash(p01, p23); - - dPath = new bytes32[](2); - dPath[0] = uLeaf; - dPath[1] = p23; - - uPath = new bytes32[](2); - uPath[0] = dLeaf; - uPath[1] = p23; - - ePath = new bytes32[](2); - ePath[0] = idLeaf; - ePath[1] = p01; - - idPath = new bytes32[](2); - idPath[0] = eLeaf; - idPath[1] = p01; - } - - function _buildProof(string memory handle, string memory userId, address userAddr, address walletAddr) - internal - view - returns (Registry.FullTlsProof memory) - { - (bytes32 root, bytes32[] memory dp, bytes32[] memory up, bytes32[] memory ep, bytes32[] memory idp) = - _buildTree(handle, userId); - - bytes32 domainHash = keccak256(bytes(PLATFORM)); - bytes32 cr = bytes32(uint256(0xC1)); - bytes32 sr = bytes32(uint256(0x5E)); - bytes memory sek = hex"deadbeef"; - uint256 ts = block.timestamp; - - bytes32 nd = - keccak256(abi.encode(block.chainid, address(registry), domainHash, cr, sr, keccak256(sek), root, ts)); - bytes32 nEth = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", nd)); - (uint8 nv, bytes32 nr, bytes32 ns) = vm.sign(NOTARY_KEY, nEth); - - bytes32 bd = keccak256(abi.encode(userAddr, walletAddr, root, ts)); - bytes32 bEth = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", bd)); - (uint8 bv, bytes32 br, bytes32 bs) = vm.sign(BACKEND_KEY, bEth); - - return Registry.FullTlsProof({ - notarySignature: abi.encodePacked(nr, ns, nv), - backendSignature: abi.encodePacked(br, bs, bv), - userAddress: userAddr, - walletAddress: walletAddr, - domainHash: domainHash, - clientRandom: cr, - serverRandom: sr, - serverEphemeralKey: sek, - transcriptRoot: root, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: idp - }); - } - - function _execSig(uint256 key, address walletAddr, address target, bytes memory data) - internal - view - returns (bytes[] memory sigs) - { - uint256 n = WebWallet(payable(walletAddr)).nonce(); - bytes32 digest = keccak256(abi.encode(target, uint256(0), keccak256(data), n, block.chainid, walletAddr)); - bytes32 eth = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, eth); - sigs = new bytes[](1); - sigs[0] = abi.encodePacked(r, s, v); - } - - function _stripSelector(bytes memory data) internal pure returns (bytes memory out) { - out = new bytes(data.length - 4); - for (uint256 i = 0; i < out.length; i++) { - out[i] = data[i + 4]; - } - } - - /// Backend-signed walletAddress blocks proof replay from a different wallet. - function test_TLS_walletBinding_blocksAttackerReplay_allowsVictimSubmission() public { - // Bootstrap victim's wallet via register_session (walletAddress = 0). - Registry.FullTlsProof memory vBoot = - _buildProof("victim_boot", _idFor("victim_boot"), victimBootAddr, address(0)); - registry.register_session(vBoot, PLATFORM, "victim_boot", _idFor("victim_boot"), ENDPOINT); - address victimWallet = registry.resolve(PLATFORM, "victim_boot"); - - // Bootstrap attacker's wallet. - Registry.FullTlsProof memory aBoot = - _buildProof("attacker_boot", _idFor("attacker_boot"), attackerBootAddr, address(0)); - registry.register_session(aBoot, PLATFORM, "attacker_boot", _idFor("attacker_boot"), ENDPOINT); - address attackerWallet = registry.resolve(PLATFORM, "attacker_boot"); - assertTrue(attackerWallet != victimWallet); - - // VICTIM composes a link tx — backend signs the proof bound to W_v. - Registry.FullTlsProof memory linkProof = - _buildProof(VICTIM_TARGET, _idFor(VICTIM_TARGET), victimLinkAddr, victimWallet); - bytes memory victimLinkData = abi.encodeCall( - Registry.linkIdentity, (PLATFORM, VICTIM_TARGET, _idFor(VICTIM_TARGET), linkProof, ENDPOINT) - ); - - // ATTACKER intercepts the raw calldata. - bytes memory args = _stripSelector(victimLinkData); - ( - string memory dPlatform, - string memory dHandle, - string memory dUserId, - Registry.FullTlsProof memory stolenProof, - string memory dEndpoint - ) = abi.decode(args, (string, string, string, Registry.FullTlsProof, string)); - assertEq(stolenProof.walletAddress, victimWallet); - - // ATTACKER tries to replay from their own wallet. Contract requires - // proof.walletAddress == msg.sender, which is now W_atk → revert. - bytes memory attackerLinkData = - abi.encodeCall(Registry.linkIdentity, (dPlatform, dHandle, dUserId, stolenProof, dEndpoint)); - bytes[] memory attackerSigs = _execSig(ATTACKER_BOOT_KEY, attackerWallet, address(registry), attackerLinkData); - vm.expectRevert(); - WebWallet(payable(attackerWallet)).execute(address(registry), 0, attackerLinkData, attackerSigs); - - assertEq(registry.resolve(PLATFORM, VICTIM_TARGET), address(0)); - - // VICTIM's own submission goes through (msg.sender == W_v). - bytes[] memory victimSigs = _execSig(VICTIM_BOOT_KEY, victimWallet, address(registry), victimLinkData); - WebWallet(payable(victimWallet)).execute(address(registry), 0, victimLinkData, victimSigs); - assertEq(registry.resolve(PLATFORM, VICTIM_TARGET), victimWallet); - } -} diff --git a/solidity/archived/login/test/WalletFactory.t.sol b/solidity/archived/login/test/WalletFactory.t.sol deleted file mode 100644 index 1cb7c555..00000000 --- a/solidity/archived/login/test/WalletFactory.t.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {WalletFactory} from "../WalletFactory.sol"; -import {WebWallet} from "../WebWallet.sol"; - -contract WalletFactoryTest is Test { - WalletFactory factory; - WebWallet walletImpl; - - address owner = makeAddr("owner"); - address registry = makeAddr("registry"); - address stranger = makeAddr("stranger"); - - bytes32 constant IDENTITY_ALICE = keccak256(abi.encode("tiktok", "alice")); - - function setUp() public { - walletImpl = new WebWallet(); - WalletFactory fImpl = new WalletFactory(); - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (owner, address(walletImpl), registry)) - ) - ) - ); - } - - function test_initialize_setsBeacon() public view { - assertTrue(address(factory.beacon()) != address(0)); - } - - function test_initialize_setsRegistry() public view { - assertEq(factory.registry(), registry); - } - - function test_deployWebWallet_onlyRegistry() public { - vm.expectRevert(WalletFactory.OnlyRegistry.selector); - factory.deploy_web_wallet("tiktok", "alice", "alice_id"); - } - - function test_deployWebWallet_success() public { - vm.prank(registry); - address wallet = factory.deploy_web_wallet("tiktok", "alice", "alice_id"); - - assertTrue(wallet != address(0)); - assertTrue(factory.isRegisteredWallet(wallet)); - - WebWallet w = WebWallet(payable(wallet)); - assertEq(w.registry(), registry); - assertEq(w.identityCount(), 0); // identity tracked only after session registration - assertEq(w.threshold(), 1); - } - - function test_deployWebWallet_multipleWalletsPerIdentity() public { - vm.startPrank(registry); - address w1 = factory.deploy_web_wallet("tiktok", "alice", "alice_id"); - address w2 = factory.deploy_web_wallet("tiktok", "alice", "alice_id"); - vm.stopPrank(); - - assertTrue(w1 != w2); - assertTrue(factory.isRegisteredWallet(w1)); - assertTrue(factory.isRegisteredWallet(w2)); - } - - function test_upgradeWalletImplementation_onlyOwner() public { - WebWallet newImpl = new WebWallet(); - vm.expectRevert(); - factory.upgradeWalletImplementation(address(newImpl)); - } - - function test_upgradeWalletImplementation_success() public { - WebWallet newImpl = new WebWallet(); - vm.prank(owner); - factory.upgradeWalletImplementation(address(newImpl)); - assertEq(factory.beacon().implementation(), address(newImpl)); - } - - function test_setRegistry_onlyOwner() public { - vm.expectRevert(); - factory.setRegistry(makeAddr("newRegistry")); - } - - function test_setRegistry_success() public { - address newRegistry = makeAddr("newRegistry"); - vm.prank(owner); - factory.setRegistry(newRegistry); - assertEq(factory.registry(), newRegistry); - } -} diff --git a/solidity/archived/login/test/WebWallet.t.sol b/solidity/archived/login/test/WebWallet.t.sol deleted file mode 100644 index 95bd70ff..00000000 --- a/solidity/archived/login/test/WebWallet.t.sol +++ /dev/null @@ -1,723 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {WalletFactory} from "../WalletFactory.sol"; -import {WebWallet} from "../WebWallet.sol"; - -/// @dev Mock registry that accepts register_session calls. -contract MockRegistryForWalletTest { - function registerSession( - WebWallet wallet, - string calldata platform, - string calldata handle, - string calldata userId, - address sessionAddr - ) external { - wallet.register_session(platform, handle, userId, sessionAddr); - } - - function reregisterById( - WebWallet wallet, - string calldata platform, - string calldata handle, - string calldata userId, - address sessionAddr - ) external { - wallet.reregisterSessionById(platform, handle, userId, sessionAddr); - } - - fallback() external payable {} - receive() external payable {} -} - -contract WebWalletTest is Test { - WebWallet wallet; - MockRegistryForWalletTest mockRegistryContract; - address mockRegistry; - - // Sessions are keyed by the immutable (platform, userId), NOT the handle. - string constant ALICE_ID = "alice_id"; - bytes32 constant IDENTITY_ALICE_TT = keccak256(abi.encode("tiktok", ALICE_ID)); - bytes32 constant IDENTITY_ALICE_KEY = keccak256(abi.encode("tiktok", ALICE_ID)); - - uint256 sessionPk = 0xDEAD; - address sessionAddr; - - function setUp() public { - sessionAddr = vm.addr(sessionPk); - - mockRegistryContract = new MockRegistryForWalletTest(); - mockRegistry = address(mockRegistryContract); - - wallet = new WebWallet(); - wallet.initialize(mockRegistry, address(0), "tiktok", "alice"); - - // Register session via mock registry - mockRegistryContract.registerSession(wallet, "tiktok", "alice", ALICE_ID, sessionAddr); - - vm.warp(1_000_000); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _execute(address target, uint256 value, bytes memory data, uint256[] memory pks) internal { - uint256 currentNonce = wallet.nonce(); - bytes32 digest = - keccak256(abi.encode(target, value, keccak256(data), currentNonce, block.chainid, address(wallet))); - - bytes[] memory sigs = new bytes[](pks.length); - for (uint256 i = 0; i < pks.length; i++) { - sigs[i] = _sign(pks[i], digest); - } - - wallet.execute(target, value, data, sigs); - } - - // ── register_session ──────────────────────────────────────────── - - function test_registerSession_storesSession() public view { - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 1); - assertEq(wallet.sessionAt(IDENTITY_ALICE_KEY, 0), sessionAddr); - } - - function test_registerSession_setsSession() public view { - assertTrue(wallet.isSession(sessionAddr)); - } - - function test_registerSession_storesIdentity() public view { - assertEq(wallet.sessionIdentity(sessionAddr), IDENTITY_ALICE_KEY); - } - - function test_registerSession_revertsIfNotRegistry() public { - vm.expectRevert(WebWallet.OnlyRegistry.selector); - wallet.register_session("tiktok", "alice", ALICE_ID, sessionAddr); - } - - function test_registerSession_revertsIfZeroAddress() public { - vm.prank(mockRegistry); - vm.expectRevert("zero session"); - wallet.register_session("tiktok", "alice", ALICE_ID, address(0)); - } - - function test_registerSession_newPairAddsIdentity() public { - assertEq(wallet.identityCount(), 1); - // Register session for a new (platform, handle) pair - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "github.com", "alice", "alice_gh_id", addr2); - assertEq(wallet.identityCount(), 2); - bytes32 ghKey = keccak256(abi.encode("github.com", "alice_gh_id")); - assertTrue(wallet.isIdentity(ghKey)); - } - - function test_registerSession_existingPairJustAddsKey() public { - assertEq(wallet.identityCount(), 1); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 1); - // Register another session for same pair - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "tiktok", "alice", ALICE_ID, addr2); - assertEq(wallet.identityCount(), 1); // still 1 identity - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 2); // 2 sessions - } - - // ── initialize ────────────────────────────────────────────────── - - function test_initialize_setsRegistry() public view { - assertEq(wallet.registry(), mockRegistry); - } - - function test_initialize_setsThresholdToOne() public view { - assertEq(wallet.threshold(), 1); - } - - function test_initialize_addsFirstIdentity() public view { - assertTrue(wallet.isIdentity(IDENTITY_ALICE_TT)); - assertEq(wallet.identityCount(), 1); - } - - function test_initialize_storesIdentityFromPlatformHandle() public { - WebWallet w2 = new WebWallet(); - w2.initialize(mockRegistry, address(0), "discord", "bob"); - bytes32 key = keccak256(abi.encode("discord", "bob_id")); - // Identity is only tracked after a session is registered - assertEq(w2.identityCount(), 0); - // Register a session — identity should now appear - uint256 pk2 = 0xCAFE; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(w2, "discord", "bob", "bob_id", addr2); - assertEq(w2.identityCount(), 1); - assertEq(w2.identityAt(0), key); - } - - // ── execute ───────────────────────────────────────────────────── - - function test_execute_incrementsNonce() public { - assertEq(wallet.nonce(), 0); - - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute(mockRegistry, 0, hex"", pks); - assertEq(wallet.nonce(), 1); - } - - function test_execute_duplicatePairCountsOnce() public { - // Register a second session for the same (tiktok, alice) pair - uint256 pk2 = 0xBEEF; - mockRegistryContract.registerSession(wallet, "tiktok", "alice", ALICE_ID, vm.addr(pk2)); - - uint256[] memory pks = new uint256[](2); - pks[0] = sessionPk; - pks[1] = pk2; - - // threshold=1, 1 distinct pair → should succeed - _execute(mockRegistry, 0, hex"", pks); - assertEq(wallet.nonce(), 1); - } - - function test_execute_differentPairsCountSeparately() public { - // Register a session for a different identity - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "github.com", "alice", "alice_gh_id", addr2); - - // Set threshold to 2 - uint256[] memory pks1 = new uint256[](1); - pks1[0] = sessionPk; - _execute(address(wallet), 0, abi.encodeCall(WebWallet.setThreshold, (2)), pks1); - - // Now need 2 distinct pairs - uint256[] memory pks2 = new uint256[](2); - pks2[0] = sessionPk; - pks2[1] = pk2; - _execute(mockRegistry, 0, hex"", pks2); - assertEq(wallet.nonce(), 2); - } - - function test_execute_revokedSessionDoesNotCount() public { - // Register a second identity so we can revoke alice and test - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "github.com", "alice", "alice_gh_id", addr2); - - // Revoke the original tiktok/alice session - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute( - address(wallet), 0, abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice", ALICE_ID, sessionAddr)), pks - ); - - // Now try to use revoked session — should fail (threshold=1, 0 valid sigs) - bytes memory data = hex""; - uint256 currentNonce = wallet.nonce(); - bytes32 digest = keccak256( - abi.encode(mockRegistry, uint256(0), keccak256(data), currentNonce, block.chainid, address(wallet)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - wallet.execute(mockRegistry, 0, data, sigs); - } - - // ── revokeSession ─────────────────────────────────────────────── - - function test_revokeSession_removesLastSession_identityDisappears() public { - // Revoke via execute (self-call) - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute( - address(wallet), 0, abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice", ALICE_ID, sessionAddr)), pks - ); - - assertFalse(wallet.isSession(sessionAddr)); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 0); - assertFalse(wallet.isIdentity(IDENTITY_ALICE_TT)); - assertEq(wallet.identityCount(), 0); // identity removed from array - } - - function test_revokeSession_removesNonLastSession_identityPersists() public { - // Register a second session for the same pair - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "tiktok", "alice", ALICE_ID, addr2); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 2); - - // Revoke the first session - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute( - address(wallet), 0, abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice", ALICE_ID, sessionAddr)), pks - ); - - assertFalse(wallet.isSession(sessionAddr)); - assertTrue(wallet.isSession(addr2)); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 1); - assertTrue(wallet.isIdentity(IDENTITY_ALICE_TT)); // still active - assertEq(wallet.identityCount(), 1); // identity still in array - } - - function test_revokeSession_revertsIfNotSelf() public { - vm.expectRevert(WebWallet.OnlySelf.selector); - wallet.revokeSession("tiktok", "alice", ALICE_ID, sessionAddr); - } - - function test_revokeSession_revertsIfNotFound() public { - address fake = makeAddr("fake"); - bytes memory revokeData = abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice", ALICE_ID, fake)); - - uint256 currentNonce = wallet.nonce(); - bytes32 digest = keccak256( - abi.encode(address(wallet), uint256(0), keccak256(revokeData), currentNonce, block.chainid, address(wallet)) - ); - - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - - vm.expectRevert(); // SessionNotFound wrapped in ExecutionFailed - wallet.execute(address(wallet), 0, revokeData, sigs); - } - - // ── upgradeToLatest ───────────────────────────────────────────── - - function test_upgradeToLatest_revertsIfNotSelf() public { - vm.expectRevert("only via execute()"); - wallet.upgradeToLatest(); - } - - /// @notice Happy path: upgradeToLatest via execute() updates implementation slot. - function test_upgradeToLatest_success() public { - WebWallet implV1 = new WebWallet(); - WebWallet implV2 = new WebWallet(); - - // Deploy factory pointing to implV1 - WalletFactory fImpl = new WalletFactory(); - WalletFactory wFactory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(implV1), mockRegistry)) - ) - ) - ); - - // Deploy wallet proxy via factory (snapshot implV1) - vm.prank(mockRegistry); - WebWallet proxiedWallet = WebWallet(payable(wFactory.deploy_web_wallet("tiktok", "carol", "carol_id"))); - mockRegistryContract.registerSession(proxiedWallet, "tiktok", "carol", "carol_id", sessionAddr); - - // Upgrade beacon to implV2 - wFactory.upgradeWalletImplementation(address(implV2)); - assertEq(wFactory.latestImplementation(), address(implV2)); - - // ERC1967 impl slot - bytes32 implSlot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); - address beforeImpl = address(uint160(uint256(vm.load(address(proxiedWallet), implSlot)))); - assertEq(beforeImpl, address(implV1)); - - // Call upgradeToLatest via execute (self-call) - bytes memory data = abi.encodeCall(WebWallet.upgradeToLatest, ()); - uint256 nonce = proxiedWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(proxiedWallet), uint256(0), keccak256(data), nonce, block.chainid, address(proxiedWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - proxiedWallet.execute(address(proxiedWallet), 0, data, sigs); - - address afterImpl = address(uint160(uint256(vm.load(address(proxiedWallet), implSlot)))); - assertEq(afterImpl, address(implV2)); - } - - /// @notice State (sessions, threshold) is preserved after upgradeToLatest. - function test_upgradeToLatest_statePreserved() public { - WebWallet implV1 = new WebWallet(); - WebWallet implV2 = new WebWallet(); - - WalletFactory fImpl = new WalletFactory(); - WalletFactory wFactory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(implV1), mockRegistry)) - ) - ) - ); - - vm.prank(mockRegistry); - WebWallet proxiedWallet = WebWallet(payable(wFactory.deploy_web_wallet("tiktok", "carol", "carol_id"))); - mockRegistryContract.registerSession(proxiedWallet, "tiktok", "carol", "carol_id", sessionAddr); - - bytes32 identityKey = keccak256(abi.encode("tiktok", "carol_id")); - assertEq(proxiedWallet.sessionCount(identityKey), 1); - assertEq(proxiedWallet.threshold(), 1); - - // Upgrade beacon and call upgradeToLatest - wFactory.upgradeWalletImplementation(address(implV2)); - bytes memory data = abi.encodeCall(WebWallet.upgradeToLatest, ()); - uint256 nonce = proxiedWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(proxiedWallet), uint256(0), keccak256(data), nonce, block.chainid, address(proxiedWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - proxiedWallet.execute(address(proxiedWallet), 0, data, sigs); - - // State unchanged after upgrade - assertEq(proxiedWallet.sessionCount(identityKey), 1); - assertEq(proxiedWallet.sessionAt(identityKey, 0), sessionAddr); - assertEq(proxiedWallet.threshold(), 1); - assertTrue(proxiedWallet.isSession(sessionAddr)); - } - - /// @notice upgradeToLatest is a no-op (same impl) when factory still points to current impl. - function test_upgradeToLatest_noOpIfAlreadyLatest() public { - WebWallet implV1 = new WebWallet(); - - WalletFactory fImpl = new WalletFactory(); - WalletFactory wFactory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(implV1), mockRegistry)) - ) - ) - ); - - vm.prank(mockRegistry); - WebWallet proxiedWallet = WebWallet(payable(wFactory.deploy_web_wallet("tiktok", "carol", "carol_id"))); - mockRegistryContract.registerSession(proxiedWallet, "tiktok", "carol", "carol_id", sessionAddr); - - bytes32 implSlot = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); - address beforeImpl = address(uint160(uint256(vm.load(address(proxiedWallet), implSlot)))); - - // Don't upgrade beacon — factory still returns implV1 - bytes memory data = abi.encodeCall(WebWallet.upgradeToLatest, ()); - uint256 nonce = proxiedWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(proxiedWallet), uint256(0), keccak256(data), nonce, block.chainid, address(proxiedWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - proxiedWallet.execute(address(proxiedWallet), 0, data, sigs); - - address afterImpl = address(uint160(uint256(vm.load(address(proxiedWallet), implSlot)))); - assertEq(afterImpl, beforeImpl); - } - - // ── setThreshold ──────────────────────────────────────────────── - - function test_setThreshold_revertsIfNotSelf() public { - vm.expectRevert(WebWallet.OnlySelf.selector); - wallet.setThreshold(1); - } - - function test_setThreshold_revertsIfZero() public { - bytes memory data = abi.encodeCall(WebWallet.setThreshold, (0)); - uint256 currentNonce = wallet.nonce(); - bytes32 digest = keccak256( - abi.encode(address(wallet), uint256(0), keccak256(data), currentNonce, block.chainid, address(wallet)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - vm.expectRevert(); // "zero threshold" wrapped in ExecutionFailed - wallet.execute(address(wallet), 0, data, sigs); - } - - function test_setThreshold_revertsIfExceedsIdentityCount() public { - // wallet has 1 identity, try to set threshold to 2 - bytes memory data = abi.encodeCall(WebWallet.setThreshold, (2)); - uint256 currentNonce = wallet.nonce(); - bytes32 digest = keccak256( - abi.encode(address(wallet), uint256(0), keccak256(data), currentNonce, block.chainid, address(wallet)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - vm.expectRevert(); // ThresholdExceedsIdentityCount wrapped in ExecutionFailed - wallet.execute(address(wallet), 0, data, sigs); - } - - function test_setThreshold_successAtIdentityCount() public { - // Add a second identity - uint256 pk2 = 0xBEEF; - mockRegistryContract.registerSession(wallet, "github.com", "alice", "alice_gh_id", vm.addr(pk2)); - assertEq(wallet.identityCount(), 2); - - // Set threshold to 2 — should succeed - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute(address(wallet), 0, abi.encodeCall(WebWallet.setThreshold, (2)), pks); - assertEq(wallet.threshold(), 2); - } - - // ── register_session duplicate ────────────────────────────────── - - function test_registerSession_revertsDuplicate() public { - // sessionAddr is already registered, try to register again - vm.prank(mockRegistry); - vm.expectRevert(WebWallet.SessionAlreadyRegistered.selector); - wallet.register_session("tiktok", "alice", ALICE_ID, sessionAddr); - } - - function test_registerSession_revertsDuplicateAcrossIdentities() public { - // sessionAddr registered for tiktok/alice, try for github/alice - vm.prank(mockRegistry); - vm.expectRevert(WebWallet.SessionAlreadyRegistered.selector); - wallet.register_session("github.com", "alice", "alice_gh_id", sessionAddr); - } - - // ── execute edge cases ────────────────────────────────────────── - - function test_execute_revertsIfNonceWrong() public { - // Use nonce=999 instead of current (0) - bytes memory data = hex""; - bytes32 digest = keccak256( - abi.encode(mockRegistry, uint256(0), keccak256(data), uint256(999), block.chainid, address(wallet)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - wallet.execute(mockRegistry, 0, data, sigs); - } - - function test_execute_revertsIfChainIdWrong() public { - bytes memory data = hex""; - // Sign with wrong chainId - bytes32 digest = keccak256( - abi.encode(mockRegistry, uint256(0), keccak256(data), uint256(0), uint256(9999), address(wallet)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - wallet.execute(mockRegistry, 0, data, sigs); - } - - // ── executeBatch ──────────────────────────────────────────────── - - function test_executeBatch_revertsIfLengthMismatch() public { - address[] memory targets = new address[](2); - uint256[] memory values = new uint256[](1); // mismatch - bytes[] memory datas = new bytes[](2); - bytes[] memory sigs = new bytes[](0); - - vm.expectRevert("length mismatch"); - wallet.executeBatch(targets, values, datas, sigs); - } - - function test_executeBatch_emptyBatchSucceeds() public { - address[] memory targets = new address[](0); - uint256[] memory values = new uint256[](0); - bytes[] memory datas = new bytes[](0); - - uint256 currentNonce = wallet.nonce(); - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = - keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - - wallet.executeBatch(targets, values, datas, sigs); - assertEq(wallet.nonce(), 1); - } - - function test_executeBatch_revertsIfAnyCallFails() public { - // First call succeeds (noop to mock registry), second reverts - address[] memory targets = new address[](2); - uint256[] memory values = new uint256[](2); - bytes[] memory datas = new bytes[](2); - - targets[0] = mockRegistry; - targets[1] = address(wallet); - values[0] = 0; - values[1] = 0; - datas[0] = hex""; - datas[1] = abi.encodeCall(WebWallet.setThreshold, (0)); // will revert - - uint256 currentNonce = wallet.nonce(); - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = - keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - - vm.expectRevert(); // ExecutionFailed - wallet.executeBatch(targets, values, datas, sigs); - // Nonce should NOT have incremented - assertEq(wallet.nonce(), 0); - } - - function test_executeBatch_incrementsNonceCorrectly() public { - // Two noop calls - address[] memory targets = new address[](2); - uint256[] memory values = new uint256[](2); - bytes[] memory datas = new bytes[](2); - - targets[0] = mockRegistry; - targets[1] = mockRegistry; - values[0] = 0; - values[1] = 0; - datas[0] = hex""; - datas[1] = hex""; - - uint256 currentNonce = wallet.nonce(); - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = - keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(sessionPk, digest); - - wallet.executeBatch(targets, values, datas, sigs); - assertEq(wallet.nonce(), 1); // only +1 per batch, not per call - } - - // ── revokeSession cleans up identityKeys ──────────────────────── - - function test_revokeSession_lastSessionCleansUpIdentityKeys() public { - // Already tested above but let's verify identityAt is cleaned - assertEq(wallet.identityCount(), 1); - assertEq(wallet.identityAt(0), IDENTITY_ALICE_KEY); - - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute( - address(wallet), 0, abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice", ALICE_ID, sessionAddr)), pks - ); - - assertEq(wallet.identityCount(), 0); - } - - // ── initialize double-init ────────────────────────────────────── - - function test_initialize_revertsOnDoubleInit() public { - vm.expectRevert(); - wallet.initialize(mockRegistry, address(0), "tiktok", "alice"); - } - - // ── id-keying: rename stability ───────────────────────────────── - // - // A handle rename must NOT move a session. Sessions are keyed by the - // immutable (platform, userId); the handle is display-only. - - function test_rename_sameIdKeepsSessionsUnderOneIdentity() public { - // setUp registered tiktok/alice under ALICE_ID with display handle "alice". - // Simulate a rename: register another session under the SAME (tiktok, ALICE_ID) - // but with a NEW display handle. - uint256 pk2 = 0xBEEF; - address addr2 = vm.addr(pk2); - mockRegistryContract.registerSession(wallet, "tiktok", "alice_renamed", ALICE_ID, addr2); - - // Both sessions live under the SAME identity key — the rename did not - // create a new identity. - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 2); - assertEq(wallet.identityCount(), 1); - assertEq(wallet.sessionIdentity(addr2), IDENTITY_ALICE_KEY); - - // Revoking by the NEW display handle + same userId works, proving the key - // is the id, not the handle. - uint256[] memory pks = new uint256[](1); - pks[0] = sessionPk; - _execute( - address(wallet), - 0, - abi.encodeCall(WebWallet.revokeSession, ("tiktok", "alice_renamed", ALICE_ID, addr2)), - pks - ); - assertFalse(wallet.isSession(addr2)); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 1); - assertEq(wallet.identityCount(), 1); - } - - // ── id-keying: threshold counts distinct ids ──────────────────── - - function test_threshold_countsDistinctIdsNotSessions() public { - // Second session under the SAME id (tiktok, ALICE_ID) — different display handle. - uint256 pkSameId = 0xBEEF; - address addrSameId = vm.addr(pkSameId); - mockRegistryContract.registerSession(wallet, "tiktok", "alice_alt", ALICE_ID, addrSameId); - - // A genuinely distinct identity (github, alice_gh_id). - uint256 pkOtherId = 0xF00D; - address addrOtherId = vm.addr(pkOtherId); - mockRegistryContract.registerSession(wallet, "github.com", "alice", "alice_gh_id", addrOtherId); - assertEq(wallet.identityCount(), 2); - - // Raise threshold to 2 (2 distinct identities exist). - uint256[] memory one = new uint256[](1); - one[0] = sessionPk; - _execute(address(wallet), 0, abi.encodeCall(WebWallet.setThreshold, (2)), one); - assertEq(wallet.threshold(), 2); - - bytes memory noop = hex""; - uint256 n = wallet.nonce(); - bytes32 digest = - keccak256(abi.encode(mockRegistry, uint256(0), keccak256(noop), n, block.chainid, address(wallet))); - - // Two sessions under the SAME id → 1 distinct identity → below threshold=2. - bytes[] memory sameIdSigs = new bytes[](2); - sameIdSigs[0] = _sign(sessionPk, digest); - sameIdSigs[1] = _sign(pkSameId, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - wallet.execute(mockRegistry, 0, noop, sameIdSigs); - - // Two sessions under DISTINCT ids → 2 distinct identities → meets threshold=2. - bytes[] memory distinctSigs = new bytes[](2); - distinctSigs[0] = _sign(sessionPk, digest); - distinctSigs[1] = _sign(pkOtherId, digest); - wallet.execute(mockRegistry, 0, noop, distinctSigs); - assertEq(wallet.nonce(), 2); // setThreshold(+1) then this exec(+1) - } - - // ── id-keying: reregisterSessionById migration ────────────────── - - function test_reregisterSessionById_noOpWhenAlreadyIdKeyed() public { - // sessionAddr is already under the id key (tiktok, ALICE_ID); reregister is a no-op. - mockRegistryContract.reregisterById(wallet, "tiktok", "alice", ALICE_ID, sessionAddr); - assertTrue(wallet.isSession(sessionAddr)); - assertEq(wallet.sessionCount(IDENTITY_ALICE_KEY), 1); - assertEq(wallet.sessionIdentity(sessionAddr), IDENTITY_ALICE_KEY); - } - - function test_reregisterSessionById_migratesLegacyHandleKeyToIdKey() public { - // Seed a "legacy" session by registering with userId == handle, which yields - // the pre-upgrade key keccak256(platform, handle). - uint256 legacyPk = 0xABCD; - address legacyAddr = vm.addr(legacyPk); - mockRegistryContract.registerSession(wallet, "discord", "carol", "carol", legacyAddr); - bytes32 legacyKey = keccak256(abi.encode("discord", "carol")); - bytes32 idKey = keccak256(abi.encode("discord", "carol_real_id")); - assertEq(wallet.sessionCount(legacyKey), 1); - assertEq(wallet.sessionCount(idKey), 0); - - // Migrate it to the immutable id key. - mockRegistryContract.reregisterById(wallet, "discord", "carol", "carol_real_id", legacyAddr); - - assertEq(wallet.sessionCount(legacyKey), 0); - assertEq(wallet.sessionCount(idKey), 1); - assertEq(wallet.sessionAt(idKey, 0), legacyAddr); - assertEq(wallet.sessionIdentity(legacyAddr), idKey); - assertTrue(wallet.isSession(legacyAddr)); - } - - function test_reregisterSessionById_revertsIfNotRegistry() public { - vm.expectRevert(WebWallet.OnlyRegistry.selector); - wallet.reregisterSessionById("tiktok", "alice", ALICE_ID, sessionAddr); - } - - function test_reregisterSessionById_revertsOnEmptyUserId() public { - vm.prank(mockRegistry); - vm.expectRevert("empty userId"); - wallet.reregisterSessionById("tiktok", "alice", "", sessionAddr); - } -} diff --git a/solidity/archived/login/test/fixtures/oidc/jwks-proof.json b/solidity/archived/login/test/fixtures/oidc/jwks-proof.json deleted file mode 100644 index 891e1bbd..00000000 --- a/solidity/archived/login/test/fixtures/oidc/jwks-proof.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "notary_signature": "0xac2dd7d49bc1d763ebec55bf3ea8d4ca3057a6c990a84658cbbc6bda6c6f8fc62b8bf782ecc81fdbe17a49a9900c5ce8599d99bdd4bdba199d98a5d63d3ec7d21c", - "domain_hash": "0x59962e64d51a4d36e444d18d029aff438ce3ddb882eb151a0af0fdde3cb655ee", - "client_random": "0x0000000000000000000000000000000000000000000000000000000000000000", - "server_random": "0x0000000000000000000000000000000000000000000000000000000000000000", - "server_ephemeral_key": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transcript_root": "0xb66b4209a1c2e90f9bd4318986e32bffce84ba9cadd2690426dc0e19e78924c3", - "timestamp": 1777900352, - "domain_path": [ - "0x3a48ec7938d008c4fbdfc033b9c0ada126a46089e460e5f3af5eda1352f9a4ee", - "0x4fd66d65911c483bedf4d98f568b8f40ce2cc7b652a9cf849b269f07de10c1cd" - ], - "endpoint_path": [ - "0x2fc77341079e14a1103642484ef49d836a16a456f497722239924495e377d4a8", - "0x4fd66d65911c483bedf4d98f568b8f40ce2cc7b652a9cf849b269f07de10c1cd" - ], - "claims": [ - { - "kid": "19caaecde8f485e8f5938f48aba0ce7a3581f027", - "n_b64url": "xGoOLta_JnpIStUy0UMlbQRnMZhRZ3MJqm3TQMb2nXQwstM91mEhjjGikBJoCA4sGXyq9SQmLcicQ-ezhnTD5TCqbLhjz_NndK4IUUaUcG0eOxYMnG0LlLJhZ4BonBjA7m-xDDL-vO7wSVi0t9xbYi4abO48NqJQn65rlE-mhIc15tgLawIm9oq7yoI1jYYsn3Ik_PMp-h8W-CHiUSxDhYfkIuMtnMoBwpjGLiWjzlV-yBNce1V1iKRK7uIuyuypAL3SKAcHCfxQEfAFBUuf3pM2qGcaBCtPpW096QaXdTs8_Tjoq7DwsmaPLOtK5EKzkDUcp0noRlDWB6fPP-rUMw", - "jwk_bytes": "0x7b0a2020202020202265223a202241514142222c0a202020202020226b7479223a2022525341222c0a20202020202022616c67223a20225253323536222c0a20202020202022757365223a2022736967222c0a202020202020226e223a202278476f4f4c74615f4a6e70495374557930554d6c6251526e4d5a68525a334d4a716d3354514d62326e58517773744d39316d45686a6a47696b424a6f43413473475879713953516d4c636963512d657a686e544435544371624c686a7a5f4e6e644b344955556155634730654f78594d6e47304c6c4c4a685a34426f6e426a41376d2d7844444c2d764f3777535669307439786259693461624f34384e714a516e3635726c452d6d684963313574674c6177496d396f7137796f49316a5959736e33496b5f504d702d6838572d434869555378446859666b49754d746e4d6f4277706a474c69576a7a6c562d79424e6365315631694b524b3775497579757970414c33534b41634843667851456641464255756633704d3271476361424374507057303936516158645473385f546a6f71374477736d61504c4f744b35454b7a6b44556370306e6f526c445742366650502d72554d77222c0a202020202020226b6964223a202231396361616563646538663438356538663539333866343861626130636537613335383166303237220a202020207d", - "jwk_path": [ - "0xc757d6ad13f0c4383803188f05d92c1386c91ee1b378bfd61107e8dcd6c73e58", - "0x546e7f882e139fc1436c0b1a28055a8c72d534e32529335e9297875dc7ab05ae" - ] - }, - { - "kid": "8076ddbab415556e169154c6a4a0bdbd4469b79c", - "n_b64url": "uPQl9hY_shV81dJgVp7qJdGcOsEkLD5g6EdpBOuzzul4THxflhFAkzJACPUSUdQpOIw5pKHD9sGhnLoqnOu-n_AdbJtAI_NG56KWawoZY1YQRr6GqJxccMTeh2vEdaM6haiYKbftbCk7RWsvWia0gPhiIfC6_D3OwI6ZVIsMp-U9fTDBo2wEG5fA4DIZn_QU8TAyzi4BJbgQYXmy3LETW-90v5S3ub7WFsaGqL7NEdxER_RQ6m7L2Pl4jIjbN8bkNKeUslXYm56MH1x3hD5RqeKJoZylpgT00XVHs4br3p1iSz4t8uPvvy0Q2oNEVkXgv36i7QK4s_oe_qhZFfW_UQ", - "jwk_bytes": "0x7b0a202020202020226b7479223a2022525341222c0a2020202020202265223a202241514142222c0a20202020202022757365223a2022736967222c0a20202020202022616c67223a20225253323536222c0a202020202020226e223a20227550516c3968595f7368563831644a67567037714a6447634f73456b4c44356736456470424f757a7a756c34544878666c6846416b7a4a4143505553556451704f497735704b4844397347686e4c6f716e4f752d6e5f4164624a7441495f4e4735364b5761776f5a5931595152723647714a7863634d54656832764564614d36686169594b62667462436b37525773765769613067506869496643365f44334f7749365a5649734d702d5539665444426f327745473566413444495a6e5f5155385441797a6934424a62675159586d79334c4554572d3930763553337562375746736147714c374e45647845525f5251366d374c32506c346a496a624e38626b4e4b6555736c58596d35364d483178336844355271654b4a6f5a796c70675430305856487334627233703169537a34743875507676793051326f4e45566b58677633366937514b34735f6f655f71685a4666575f5551222c0a202020202020226b6964223a202238303736646462616234313535353665313639313534633661346130626462643434363962373963220a202020207d", - "jwk_path": [ - "0x5faffac008736e1a823a01a782add8c6911e47106674955396c01954f5f7e301", - "0x546e7f882e139fc1436c0b1a28055a8c72d534e32529335e9297875dc7ab05ae" - ] - } - ] -} \ No newline at end of file diff --git a/solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json b/solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json deleted file mode 100644 index e4e96524..00000000 --- a/solidity/archived/login/test/fixtures/oidc/jwks-snapshot.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "keys": [ - { - "e": "AQAB", - "kty": "RSA", - "alg": "RS256", - "use": "sig", - "n": "xGoOLta_JnpIStUy0UMlbQRnMZhRZ3MJqm3TQMb2nXQwstM91mEhjjGikBJoCA4sGXyq9SQmLcicQ-ezhnTD5TCqbLhjz_NndK4IUUaUcG0eOxYMnG0LlLJhZ4BonBjA7m-xDDL-vO7wSVi0t9xbYi4abO48NqJQn65rlE-mhIc15tgLawIm9oq7yoI1jYYsn3Ik_PMp-h8W-CHiUSxDhYfkIuMtnMoBwpjGLiWjzlV-yBNce1V1iKRK7uIuyuypAL3SKAcHCfxQEfAFBUuf3pM2qGcaBCtPpW096QaXdTs8_Tjoq7DwsmaPLOtK5EKzkDUcp0noRlDWB6fPP-rUMw", - "kid": "19caaecde8f485e8f5938f48aba0ce7a3581f027" - }, - { - "kty": "RSA", - "e": "AQAB", - "use": "sig", - "alg": "RS256", - "n": "uPQl9hY_shV81dJgVp7qJdGcOsEkLD5g6EdpBOuzzul4THxflhFAkzJACPUSUdQpOIw5pKHD9sGhnLoqnOu-n_AdbJtAI_NG56KWawoZY1YQRr6GqJxccMTeh2vEdaM6haiYKbftbCk7RWsvWia0gPhiIfC6_D3OwI6ZVIsMp-U9fTDBo2wEG5fA4DIZn_QU8TAyzi4BJbgQYXmy3LETW-90v5S3ub7WFsaGqL7NEdxER_RQ6m7L2Pl4jIjbN8bkNKeUslXYm56MH1x3hD5RqeKJoZylpgT00XVHs4br3p1iSz4t8uPvvy0Q2oNEVkXgv36i7QK4s_oe_qhZFfW_UQ", - "kid": "8076ddbab415556e169154c6a4a0bdbd4469b79c" - } - ] -} diff --git a/solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol b/solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol deleted file mode 100644 index 35c0ba9c..00000000 --- a/solidity/archived/login/test/oidc/GoogleOidcVerifierFlow.t.sol +++ /dev/null @@ -1,616 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {HonkVerifier} from "../../oidc/Verifier.sol"; -import {GoogleOidcVerifier} from "../../oidc/GoogleOidcVerifier.sol"; -import {deployNotary} from "../../../notary/test/DeployNotary.sol"; - -/// End-to-end: TLSN-notarized JWKS rotation, then a Honk-proof JWT -/// validation through the GoogleOidcVerifier. -/// -/// Reads two on-disk fixtures: -/// * `contracts/login/test/fixtures/oidc/jwks-proof.json` - rotation proof -/// (notary-signed with the deterministic test key, address -/// `0x6f4c…b87a`). -/// * `circuits/jwt_email/target/{proof,public_inputs,proof_meta.json}` -/// - a Honk proof of a real Google JWT. These are produced by the -/// OIDC CLI on demand and *not* committed (the proof's `exp` -/// ages out within an hour, so refreshing is part of the test's -/// bootstrap). -/// -/// When the per-run circuit artifacts are absent, the test logs a -/// clear "skipped" message and returns - so the suite stays green in -/// CI environments that don't run the CLI. -contract GoogleOidcVerifierFlowTest is Test { - GoogleOidcVerifier verifier; - HonkVerifier honk; - - address constant NOTARY = 0x6f4c950442e1Af093BcfF730381E63Ae9171b87a; - address constant OWNER = address(0xA11CE); - /// Client id the verifier is initialized with. The per-run proof fixture is - /// generated for its own (env) client id, so the fixture-backed tests - /// override the expected hash from the proof's own public inputs via - /// `_configureAudienceFromFixture` rather than relying on this value. - string constant CLIENT_ID = "test-client-id.apps.googleusercontent.com"; - - GoogleOidcVerifier.NotarizedJwksProof rotProof; - GoogleOidcVerifier.JwkClaim[] rotClaims; - uint256 rotTimestamp; - - bytes honkProof; - bytes32[] honkPub; - string jwtEmail; - string jwtSub; - address sessionAddress; - uint256 jwtExp; - bool fixturesPresent; - - function setUp() public { - honk = new HonkVerifier(); - // Deploy behind a UUPS proxy (same shape as production). - GoogleOidcVerifier impl = new GoogleOidcVerifier(); - // rotate()/rotateRoots() are permissionless — no rotator wiring needed. - verifier = GoogleOidcVerifier( - address( - new ERC1967Proxy( - address(impl), - abi.encodeCall( - GoogleOidcVerifier.initialize, (honk, OWNER, address(deployNotary(OWNER, NOTARY)), CLIENT_ID) - ) - ) - ) - ); - - _loadRotation(); - fixturesPresent = _tryLoadUserProof(); - - // Always warp inside the rotation freshness window so the - // rotation-only test runs without the per-run circuit - // artifacts. When the JWT fixtures ARE present, narrow to a - // moment that's inside both freshness windows. - uint256 t = rotTimestamp + 60; - if (fixturesPresent) { - uint256 jwtAnchor = jwtExp - 60; - if (jwtAnchor > t) t = jwtAnchor; - } - vm.warp(t); - } - - // ─── Fixture loaders ────────────────────────────────────────────── - - function _loadRotation() internal { - string memory j = vm.readFile("contracts/login/test/fixtures/oidc/jwks-proof.json"); - rotProof.notarySignature = vm.parseJsonBytes(j, ".notary_signature"); - rotProof.domainHash = vm.parseJsonBytes32(j, ".domain_hash"); - rotProof.clientRandom = vm.parseJsonBytes32(j, ".client_random"); - rotProof.serverRandom = vm.parseJsonBytes32(j, ".server_random"); - rotProof.serverEphemeralKey = vm.parseJsonBytes(j, ".server_ephemeral_key"); - rotProof.transcriptRoot = vm.parseJsonBytes32(j, ".transcript_root"); - rotTimestamp = vm.parseJsonUint(j, ".timestamp"); - rotProof.timestamp = rotTimestamp; - rotProof.domainPath = vm.parseJsonBytes32Array(j, ".domain_path"); - rotProof.endpointPath = vm.parseJsonBytes32Array(j, ".endpoint_path"); - - for (uint256 i = 0; i < 16; i++) { - string memory base = string.concat(".claims[", vm.toString(i), "]"); - try this._probeKid(j, base) returns (string memory kidStr) { - GoogleOidcVerifier.JwkClaim memory c; - c.kid = bytes(kidStr); - c.nB64url = bytes(vm.parseJsonString(j, string.concat(base, ".n_b64url"))); - c.jwkBytes = vm.parseJsonBytes(j, string.concat(base, ".jwk_bytes")); - c.jwkPath = vm.parseJsonBytes32Array(j, string.concat(base, ".jwk_path")); - rotClaims.push(c); - } catch { - break; - } - } - require(rotClaims.length > 0, "no rotation claims"); - } - - function _probeKid(string memory j, string memory base) external view returns (string memory) { - return vm.parseJsonString(j, string.concat(base, ".kid")); - } - - function _tryLoadUserProof() internal returns (bool) { - try this._readBinary("circuits/jwt_email/target/proof") returns (bytes memory proof) { - honkProof = proof; - } catch { - return false; - } - bytes memory raw; - try this._readBinary("circuits/jwt_email/target/public_inputs") returns (bytes memory pub) { - raw = pub; - } catch { - return false; - } - // 28 public inputs: modulus[18] + email[2] + nonce[2] + sub[1] - // + exp[1] + audience_hash[2] + chain_id[1] + registry_addr[1]. - if (raw.length != 28 * 32) return false; - for (uint256 i = 0; i < 28; i++) { - bytes32 v; - assembly { - v := mload(add(add(raw, 32), mul(i, 32))) - } - honkPub.push(v); - } - string memory meta; - try this._readText("circuits/jwt_email/target/proof_meta.json") returns (string memory m) { - meta = m; - } catch { - return false; - } - jwtEmail = vm.parseJsonString(meta, ".email"); - jwtSub = vm.parseJsonString(meta, ".sub"); - sessionAddress = vm.parseAddress(vm.parseJsonString(meta, ".session_address")); - jwtExp = vm.parseJsonUint(meta, ".exp"); - return true; - } - - function _readBinary(string calldata path) external view returns (bytes memory) { - return vm.readFileBinary(path); - } - - function _readText(string calldata path) external view returns (string memory) { - return vm.readFile(path); - } - - function _userProofBytes() internal view returns (bytes memory) { - GoogleOidcVerifier.UserProof memory u = GoogleOidcVerifier.UserProof({ - honkProof: honkProof, publicInputs: honkPub, email: jwtEmail, sessionKey: sessionAddress, sub: jwtSub - }); - return abi.encode(u); - } - - // ─── Tests ──────────────────────────────────────────────────────── - - /// Rotation succeeds and at least one kid lands in storage. Doesn't - /// require the per-run circuit artifacts. - function test_rotation_installs_kids() public { - vm.prank(OWNER); - verifier.rotate(rotProof, rotClaims); - bytes32 kid0Hash = keccak256(rotClaims[0].kid); - assertGt(verifier.expiresAtKid(kid0Hash), block.timestamp, "rotation didn't store kid"); - } - - /// Rotation is PERMISSIONLESS: any caller may submit. This is the point of - /// removing the rotator role — a third-party keeper must be able to keep - /// JWKS fresh without being allowlisted by us. - function test_rotation_permissionless_anyCaller() public { - vm.prank(address(0xBADBAD)); - verifier.rotate(rotProof, rotClaims); - assertGt( - verifier.expiresAtKid(keccak256(rotClaims[0].kid)), - block.timestamp, - "unauthorized caller should have been able to rotate" - ); - } - - /// Re-submitting the same proof is IDEMPOTENT, not a revert. With an open - /// caller set a one-shot nullifier would itself be the attack: a front-runner - /// could consume the digest and brick the honest keeper. - function test_rotation_replay_isIdempotent() public { - verifier.rotate(rotProof, rotClaims); - bytes32 kid0 = keccak256(rotClaims[0].kid); - bytes32 mod0 = verifier.modulusOfKid(kid0); - - vm.prank(address(0xBADBAD)); - verifier.rotate(rotProof, rotClaims); // must NOT revert - - assertEq(verifier.modulusOfKid(kid0), mod0, "replay changed the modulus"); - assertGt(verifier.expiresAtKid(kid0), block.timestamp); - } - - /// A partial submission must not block a later fuller one — the griefing - /// vector the digest nullifier would have created once anyone can call. - function test_rotation_subsetDoesNotBlockFullRotation() public { - GoogleOidcVerifier.JwkClaim[] memory empty = new GoogleOidcVerifier.JwkClaim[](0); - vm.prank(address(0xBADBAD)); - verifier.rotate(rotProof, empty); // front-runner submits nothing useful - - verifier.rotate(rotProof, rotClaims); // honest keeper still lands - assertGt( - verifier.expiresAtKid(keccak256(rotClaims[0].kid)), - block.timestamp, - "subset submission blocked the real rotation" - ); - } - - /// What actually prevents an old proof regressing a newer modulus is the - /// freshness window, NOT the caller gate — so it must still bite. - function test_rotation_staleProof_stillRejected() public { - vm.warp(block.timestamp + verifier.FRESHNESS_WINDOW() + 1); - vm.expectRevert(GoogleOidcVerifier.StaleProof.selector); - verifier.rotate(rotProof, rotClaims); - } - - /// Replaying an OLDER proof inside the freshness window must not roll a kid - /// back to a modulus Google has already retired, nor re-extend its TTL. The - /// caller gate used to make this unreachable; with rotation permissionless - /// anyone can replay a still-fresh proof, so the per-kid `rotatedAtKid` - /// guard is what holds the invariant. - /// - /// Uses a verifier whose notary key the test controls, so it can re-sign the - /// fixture's transcript at a later timestamp (the claims still merkle-verify - /// against the same root). - function test_rotation_olderProofDoesNotRollBackNewer() public { - uint256 notaryPk = 0xB0B; - GoogleOidcVerifier v = _deployWithNotary(vm.addr(notaryPk)); - - GoogleOidcVerifier.NotarizedJwksProof memory older = rotProof; - older.notarySignature = _sign(notaryPk, _digest(older)); - - GoogleOidcVerifier.NotarizedJwksProof memory newer = rotProof; - newer.timestamp = rotProof.timestamp + 10 minutes; - newer.notarySignature = _sign(notaryPk, _digest(newer)); - - // Newer rotation lands first. - vm.warp(newer.timestamp + 1); - v.rotate(newer, rotClaims); - - bytes32 kid0 = keccak256(rotClaims[0].kid); - uint256 provenAfterNewer = v.rotatedAtKid(kid0); - bytes32 modulusAfterNewer = v.modulusOfKid(kid0); - uint256 expiryAfterNewer = v.expiresAtKid(kid0); - assertEq(provenAfterNewer, newer.timestamp, "newer rotation did not record its timestamp"); - - // Anyone replays the OLDER, still-fresh proof later. - vm.warp(older.timestamp + 50 minutes); - vm.prank(address(0xBADBAD)); - v.rotate(older, rotClaims); - - assertEq(v.rotatedAtKid(kid0), provenAfterNewer, "older proof overwrote the provenance stamp"); - assertEq(v.modulusOfKid(kid0), modulusAfterNewer, "older proof rolled the modulus back"); - assertEq(v.expiresAtKid(kid0), expiryAfterNewer, "older proof re-extended the TTL"); - } - - function _deployWithNotary(address notary_) internal returns (GoogleOidcVerifier) { - GoogleOidcVerifier impl = new GoogleOidcVerifier(); - return GoogleOidcVerifier( - address( - new ERC1967Proxy( - address(impl), - abi.encodeCall( - GoogleOidcVerifier.initialize, (honk, OWNER, address(deployNotary(OWNER, notary_)), CLIENT_ID) - ) - ) - ) - ); - } - - /// Mirrors GoogleOidcVerifier._notaryDigest — deliberately an independent - /// reimplementation so a change to the digest breaks this test loudly. - function _digest(GoogleOidcVerifier.NotarizedJwksProof memory p) internal pure returns (bytes32) { - return keccak256( - abi.encode( - p.domainHash, - p.clientRandom, - p.serverRandom, - keccak256(p.serverEphemeralKey), - p.transcriptRoot, - p.timestamp - ) - ); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - /// Resubmitting the exact reading already applied writes nothing — in - /// particular it does NOT re-stamp the TTL, so spamming one proof for its - /// whole freshness window cannot stretch trust by even a second. - function test_rotation_identicalResubmission_doesNotRestampTtl() public { - verifier.rotate(rotProof, rotClaims); - bytes32 kid0 = keccak256(rotClaims[0].kid); - uint256 expiry = verifier.expiresAtKid(kid0); - - vm.warp(rotTimestamp + 50 minutes); // still inside the freshness window - vm.prank(address(0xBADBAD)); - verifier.rotate(rotProof, rotClaims); - - assertEq(verifier.expiresAtKid(kid0), expiry, "replay re-stamped the TTL"); - } - - /// A rotation that hands a kid a NEW key retires the key it used to carry - /// immediately — `_verifyAndExtract` resolves by modulus, so leaving the - /// old entry would keep a retired key usable for the rest of its TTL. - function test_rotation_replacingAKidRetiresItsOldKey() public { - uint256 notaryPk = 0xB0B; - GoogleOidcVerifier v = _deployWithNotary(vm.addr(notaryPk)); - - bytes32 first = _installKid(v, notaryPk, "kid-1", "modulus-one", block.timestamp); - assertGt(v.trustedHashExpiresAt(first), 0, "first key trusted"); - - bytes32 second = _installKid(v, notaryPk, "kid-1", "modulus-two", block.timestamp + 1); - assertGt(v.trustedHashExpiresAt(second), 0, "second key trusted"); - assertEq(v.trustedHashExpiresAt(first), 0, "the replaced key must lose trust NOW"); - } - - /// Time alone retires a key, and anyone may then prune the bookkeeping. - /// The monotonic `rotatedAtKid` floor survives the prune, so an old - /// reading still cannot resurrect what time retired. - function test_prune_permissionless_reclaimsExpiredKeys() public { - verifier.rotate(rotProof, rotClaims); - bytes32 kid0 = keccak256(rotClaims[0].kid); - bytes32 mod0 = verifier.modulusOfKid(kid0); - - vm.warp(verifier.expiresAtKid(kid0)); // the stamp is spent - vm.prank(address(0xBADBAD)); - verifier.prune(); - - assertEq(verifier.modulusOfKid(kid0), bytes32(0), "kid still tracked"); - assertEq(verifier.trustedHashExpiresAt(mod0), 0, "spent stamp not cleared"); - assertEq(verifier.currentRoots().length, 0, "enumeration not reclaimed"); - assertEq(verifier.rotatedAtKid(kid0), rotTimestamp, "provenance floor lost"); - } - - /// Pruning is a no-op while the keys are alive. - function test_prune_leavesLiveKeysAlone() public { - verifier.rotate(rotProof, rotClaims); - uint256 before = verifier.currentRoots().length; - verifier.prune(); - assertEq(verifier.currentRoots().length, before, "a live key was pruned"); - } - - /// What a keeper reads: the whole list in one call, the freshest reading - /// mark, and the single needs-rotation bit. - function test_keeperViews_describeRotationState() public { - assertTrue(verifier.needsRotation(), "an empty list needs a rotation"); - assertEq(verifier.freshestObservedAt(), 0); - - verifier.rotate(rotProof, rotClaims); - - assertFalse(verifier.needsRotation(), "a fresh rotation buys quiet"); - assertEq(verifier.freshestObservedAt(), rotTimestamp, "the mark is the reading's timestamp"); - - GoogleOidcVerifier.RootInfo[] memory infos = verifier.currentRoots(); - assertGt(infos.length, 0); - for (uint256 i = 0; i < infos.length; i++) { - assertEq(infos[i].modulusHash, verifier.modulusOfKid(infos[i].kidHash)); - assertEq(infos[i].observedAt, rotTimestamp); - assertEq(infos[i].expiresAt, verifier.expiresAtKid(infos[i].kidHash)); - } - - bytes32 kid0 = keccak256(rotClaims[0].kid); - vm.warp(verifier.expiresAtKid(kid0) - verifier.RENEWAL_MARGIN()); - assertTrue(verifier.needsRotation(), "the margin should trip before expiry does"); - } - - /// The owner's only lever over the key list: removing trust early. It is - /// owner-gated, and rotation (which anyone can submit) undoes it. - function test_untrustModulus_ownerOnly_removesTrust() public { - verifier.rotate(rotProof, rotClaims); - bytes32 mod0 = verifier.modulusOfKid(keccak256(rotClaims[0].kid)); - - vm.prank(address(0xBADBAD)); - vm.expectRevert(); - verifier.untrustModulus(mod0); - - vm.prank(OWNER); - verifier.untrustModulus(mod0); - assertEq(verifier.trustedHashExpiresAt(mod0), 0, "trust should be gone"); - } - - // ─── Synthetic-reading helpers (mirror IdentityJwksRoots.t.sol) ── - - /// Install `kid` carrying a key derived from `seed` into `v`, via a full - /// synthetic notarized reading signed with `notaryPk` at `provedAt`. - function _installKid( - GoogleOidcVerifier v, - uint256 notaryPk, - string memory kid, - string memory seed, - uint256 provedAt - ) internal returns (bytes32) { - bytes memory nB64 = _syntheticModulus(seed); - bytes memory jwk = abi.encodePacked('{"kid":"', kid, '","n":"', nB64, '"}'); - - bytes32 domainLeaf = _syntheticLeaf("domain:", bytes("www.googleapis.com")); - bytes32 endpointLeaf = _syntheticLeaf("endpoint:", bytes("/oauth2/v3/certs")); - bytes32 jwkLeaf = _syntheticLeaf("recv:", jwk); - bytes32 pair = _syntheticHashPair(domainLeaf, endpointLeaf); - bytes32 root = _syntheticHashPair(pair, jwkLeaf); - - GoogleOidcVerifier.NotarizedJwksProof memory p; - p.domainHash = keccak256(bytes("www.googleapis.com")); - p.clientRandom = keccak256("client"); - p.serverRandom = keccak256("server"); - p.serverEphemeralKey = bytes("ephemeral-key"); - p.transcriptRoot = root; - p.timestamp = provedAt; - p.domainPath = new bytes32[](2); - p.domainPath[0] = endpointLeaf; - p.domainPath[1] = jwkLeaf; - p.endpointPath = new bytes32[](2); - p.endpointPath[0] = domainLeaf; - p.endpointPath[1] = jwkLeaf; - p.notarySignature = _sign(notaryPk, _digest(p)); - - GoogleOidcVerifier.JwkClaim[] memory one = new GoogleOidcVerifier.JwkClaim[](1); - one[0].jwkBytes = jwk; - one[0].jwkPath = new bytes32[](1); - one[0].jwkPath[0] = pair; - one[0].kid = bytes(kid); - one[0].nB64url = nB64; - - vm.warp(provedAt + 1); - v.rotate(p, one); - return v.modulusOfKid(keccak256(bytes(kid))); - } - - /// A 2048-bit modulus written straight in base64url: 342 characters decode - /// to exactly the 256 bytes the contract requires. - function _syntheticModulus(string memory seed) internal pure returns (bytes memory out) { - bytes memory alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - bytes32 filler = keccak256(bytes(seed)); - out = new bytes(342); - for (uint256 i = 0; i < 342; i++) { - out[i] = alphabet[uint8(filler[i % 32]) & 0x3f]; - } - } - - function _syntheticLeaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _syntheticHashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - /// Renouncing ownership is disabled — it would brick the UUPS upgrade path. - function test_renounceOwnership_disabled() public { - vm.prank(OWNER); - vm.expectRevert(bytes("renounce disabled")); - verifier.renounceOwnership(); - } - - // ─── UUPS upgrade ─────────────────────────────────────────────── - - function test_upgrade_revertsIfNotOwner() public { - GoogleOidcVerifier newImpl = new GoogleOidcVerifier(); - vm.prank(address(0xBEEF)); - vm.expectRevert(); - verifier.upgradeToAndCall(address(newImpl), ""); - } - - function test_upgrade_byOwner_preservesState() public { - // Seed state, then upgrade and confirm it survives. - verifier.rotate(rotProof, rotClaims); - bytes32 kid0Hash = keccak256(rotClaims[0].kid); - uint256 expiryBefore = verifier.expiresAtKid(kid0Hash); - assertGt(expiryBefore, 0, "precondition: kid installed"); - - GoogleOidcVerifier newImpl = new GoogleOidcVerifier(); - vm.prank(OWNER); - verifier.upgradeToAndCall(address(newImpl), ""); - - assertEq(verifier.owner(), OWNER, "owner preserved"); - assertEq(verifier.expiresAtKid(kid0Hash), expiryBefore, "rotated kid preserved"); - // Rotation is permissionless and idempotent across the upgrade. - verifier.rotate(rotProof, rotClaims); - assertEq(verifier.expiresAtKid(kid0Hash), expiryBefore, "replay changed state after upgrade"); - } - - /// User-side verification fails when no rotation has happened (the - /// modulus is not yet trusted). - function test_verify_fails_before_rotation() public { - if (!fixturesPresent) { - emit log("SKIPPED: circuits/jwt_email/target/proof artifacts absent - run oidc-noir-cli to refresh"); - return; - } - vm.expectRevert(GoogleOidcVerifier.UntrustedModulus.selector); - verifier.verifyAndExtract(_userProofBytes()); - } - - /// Full pipeline: rotate, then verify. The verifier returns the - /// validated email plaintext (matches the Registry's `(platform, - /// handle)` storage shape - see Registry.register_session_oidc). - function test_full_pipeline_rotate_then_verify() public { - if (!fixturesPresent) { - emit log("SKIPPED: circuits/jwt_email/target/proof artifacts absent - run oidc-noir-cli to refresh"); - return; - } - // Configure the verifier with the plaintext client id the proof fixture - // was generated for (GMAIL_CLIENT_ID, the same value dev-start feeds the - // prover and the deploy). This drives the real setExpectedAudience path - // (on-chain SHA-256), so a passing proof proves the contract's audience - // check — hashing AND the 16-byte half split — matches the circuit, not - // a value echoed back from the proof itself. - string memory clientId = vm.envOr("GMAIL_CLIENT_ID", string("")); - if (bytes(clientId).length == 0) { - emit log("SKIPPED: set GMAIL_CLIENT_ID (the client id the proof fixture was generated for) to run this test"); - return; - } - vm.prank(OWNER); - verifier.rotate(rotProof, rotClaims); - vm.prank(OWNER); - verifier.setExpectedAudience(clientId); - (string memory handle, address sessionKey, uint256 expiresAt, string memory userId) = verifier.verifyDirect( - GoogleOidcVerifier.UserProof({ - honkProof: honkProof, publicInputs: honkPub, email: jwtEmail, sessionKey: sessionAddress, sub: jwtSub - }) - ); - assertEq(handle, jwtEmail, "verifier should hand back the validated email plaintext"); - assertEq(sessionKey, sessionAddress, "session key passes through"); - assertEq(expiresAt, jwtExp, "exp comes from the JWT public input"); - assertEq(userId, jwtSub, "sub comes from the JWT public input"); - } - - /// Confused-deputy guard: a valid proof whose `aud` is some OTHER Google - /// OAuth app is rejected. Same fixture, but the verifier is configured for a - /// different client id, so its audience-hash public inputs don't match. - function test_verify_rejects_wrong_audience() public { - if (!fixturesPresent) { - emit log("SKIPPED: circuits/jwt_email/target/proof artifacts absent - run oidc-noir-cli to refresh"); - return; - } - vm.prank(OWNER); - verifier.rotate(rotProof, rotClaims); - // Configure a different app than the one the proof was minted for. - vm.prank(OWNER); - verifier.setExpectedAudience("some-other-app.apps.googleusercontent.com"); - vm.expectRevert(GoogleOidcVerifier.WrongAudience.selector); - verifier.verifyDirect( - GoogleOidcVerifier.UserProof({ - honkProof: honkProof, publicInputs: honkPub, email: jwtEmail, sessionKey: sessionAddress, sub: jwtSub - }) - ); - } - - // ─── Audience configuration ───────────────────────────────────── - - /// initialize pins the accepted client id: the stored hash is SHA-256 of it. - function test_initialize_configuresAudience() public view { - assertEq(verifier.expectedAudienceHash(), sha256(bytes(CLIENT_ID)), "init should hash the client id"); - } - - /// The owner can rotate which app may register; the hash tracks the id. - function test_setExpectedAudience_hashesClientId() public { - string memory newId = "another-app.apps.googleusercontent.com"; - vm.prank(OWNER); - verifier.setExpectedAudience(newId); - assertEq(verifier.expectedAudienceHash(), sha256(bytes(newId)), "hash should follow the new client id"); - } - - /// Audience config is owner-gated (both the string and raw-hash setters). - function test_setExpectedAudience_onlyOwner() public { - vm.prank(address(0xBADBAD)); - vm.expectRevert(); - verifier.setExpectedAudience("x.apps.googleusercontent.com"); - - vm.prank(address(0xBADBAD)); - vm.expectRevert(); - verifier.setExpectedAudienceHash(bytes32(uint256(1))); - } - - /// An empty client id / zero hash is rejected — never leaves the verifier - /// silently accepting nothing (or, via the zero-guard, everything). - function test_setExpectedAudience_rejectsEmpty() public { - vm.prank(OWNER); - vm.expectRevert(GoogleOidcVerifier.EmptyAudience.selector); - verifier.setExpectedAudience(""); - - vm.prank(OWNER); - vm.expectRevert(GoogleOidcVerifier.EmptyAudience.selector); - verifier.setExpectedAudienceHash(bytes32(0)); - } - - /// A client id longer than the circuit's AUDIENCE_MAX (128) is rejected at - /// config time — otherwise it would silently make every proof revert. - function test_setExpectedAudience_rejectsTooLong() public { - string memory tooLong = new string(uint256(verifier.MAX_AUDIENCE_BYTES()) + 1); - vm.prank(OWNER); - vm.expectRevert(GoogleOidcVerifier.AudienceTooLong.selector); - verifier.setExpectedAudience(tooLong); - - // Exactly MAX_AUDIENCE_BYTES is still accepted (boundary). - string memory atLimit = new string(uint256(verifier.MAX_AUDIENCE_BYTES())); - vm.prank(OWNER); - verifier.setExpectedAudience(atLimit); - assertEq(verifier.expectedAudienceHash(), sha256(bytes(atLimit)), "boundary length should be accepted"); - } -} diff --git a/solidity/archived/login/test/oidc/README.md b/solidity/archived/login/test/oidc/README.md deleted file mode 100644 index 88bc32da..00000000 --- a/solidity/archived/login/test/oidc/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# OIDC contract tests - -Tests for `contracts/login/oidc/IOidcVerifier.sol` and its implementers -(`GoogleOidcVerifier.sol`) plus the new `Registry.register_session_oidc` -entry point. - -## Running - -```bash -forge test -vv -``` - -(These run in the default profile — the OIDC verifier compiles fine under -`via_ir = true` with the current solc/bb toolchain.) - -## Fixtures - -Two kinds of fixtures, in different states of permanence: - -### Committed fixtures (`contracts/login/test/fixtures/oidc/`) - -- `jwks-snapshot.json` — verbatim Google JWKS response, captured once at - fixture-generation time. The rotation proof binds to this exact byte - sequence via Merkle leaves. -- `jwks-proof.json` — TLSN-notarized attestation over the snapshot, - signed by the deterministic test notary `0x6f4c…b87a` (privkey - `0x00…0042`). The `timestamp` in this proof must overlap with the - user JWT's `exp` for the integration test to pass. Freshness window: - `jwksProof.timestamp + ROTATION_TTL >= jwtExp > block.timestamp` - (see `GoogleOidcVerifier` constants). - -### Per-run fixtures (`circuits/jwt_email/target/`) - -- `proof`, `public_inputs`, `proof_meta.json` — produced by running - the OIDC prover CLI from the original monorepo (after the Rust lift - completes). These ARE gitignored: each is bound to a specific OAuth - code + session keypair + JWT `exp`, so they go stale after ~1 hour. - -A test that depends on these guards itself with a file-existence check -and emits a clear "skipped" message rather than failing CI. To -regenerate, run the CLI (web flow OAuths against Google live, writes -all three files): - -```bash -# in the OIDC prover CLI's directory (original monorepo) -cargo run --release -``` - -The CLI drives an OAuth → JWT → Noir witness → proof → fixture-write -pipeline; the fixtures it emits feed back into `forge test`. diff --git a/solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol b/solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol deleted file mode 100644 index 18df006b..00000000 --- a/solidity/archived/login/test/oidc/RegistryOidcLinkFlow.t.sol +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../Registry.sol"; -import {WalletFactory} from "../../WalletFactory.sol"; -import {WebWallet} from "../../WebWallet.sol"; -import {IOidcVerifier} from "../../oidc/IOidcVerifier.sol"; -import {deployNotary} from "../../../notary/test/DeployNotary.sol"; - -// A JWT expiry far in the future but well below uint256 max, so the -// `jwtExp + CLOCK_SKEW_GRACE` freshness check can't overflow. -uint256 constant FUTURE = 4_000_000_000; - -/// Stand-in OIDC verifier returning whatever the test set up, so the -/// Registry link dispatcher can be exercised without a real JWT/circuit. -contract StubOidcVerifier is IOidcVerifier { - string public handle = "alice@gmail.com"; - address public nonceAddr; // returned as the `sessionKey`/nonce field - uint256 public expiresAt = FUTURE; - string public userId = "sub_alice"; // immutable JWT `sub` - - function set(string memory h, address a, uint256 e) external { - handle = h; - nonceAddr = a; - expiresAt = e; - // Derive a unique id per handle so distinct identities map to - // distinct wallets (identity keys on the id). - userId = string(abi.encodePacked("sub_", h)); - } - - function setUserId(string memory u) external { - userId = u; - } - - function verifyAndExtract(bytes calldata) - external - view - override - returns (string memory, address, uint256, string memory) - { - return (handle, nonceAddr, expiresAt, userId); - } - - function rotateRoots(bytes calldata) external override {} - - function platformName() external pure override returns (string memory) { - return "stub-oidc"; - } -} - -/// Dispatcher-level tests for `Registry.link_identity_oidc`. The link path -/// attaches an OIDC handle to the *calling* wallet (front-run safe: the JWT -/// nonce must equal msg.sender), instead of deploying a new wallet the way -/// `register_session_oidc` does. -contract RegistryOidcLinkFlowTest is Test { - Registry registry; - WalletFactory factory; - StubOidcVerifier verifier; - - address constant OWNER = address(0xA11CE); - address constant NOTARY = address(0xB0B); - address constant BACKEND = address(0xBE); - string constant PLATFORM = "www.googleapis.com"; - - address wallet; // an existing registered WebWallet (the link target) - - function setUp() public { - WebWallet walletImpl = new WebWallet(); - WalletFactory fImpl = new WalletFactory(); - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (OWNER, address(walletImpl), address(1))) - ) - ) - ); - - Registry rImpl = new Registry(); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall( - Registry.initialize, (address(deployNotary(OWNER, NOTARY)), BACKEND, address(factory), OWNER) - ) - ) - ) - ); - - vm.prank(OWNER); - factory.setRegistry(address(registry)); - - verifier = new StubOidcVerifier(); - vm.prank(OWNER); - registry.setOidcVerifier(PLATFORM, IOidcVerifier(address(verifier))); - - // Stand up an existing wallet by registering a first handle. - verifier.set("existing@gmail.com", address(0xC0FFEE), FUTURE); - registry.register_session_oidc(PLATFORM, hex""); - wallet = registry.resolve(PLATFORM, "existing@gmail.com"); - assertTrue(wallet != address(0)); - } - - function test_link_happy_attachesHandleToCallingWallet() public { - // nonce bound to the calling wallet; fresh session key for the handle. - verifier.set("alice@gmail.com", wallet, FUTURE); - vm.prank(wallet); - registry.link_identity_oidc(PLATFORM, hex"", address(0xDECAF)); - - assertEq(registry.resolve(PLATFORM, "alice@gmail.com"), wallet); - // Sessions are keyed by the immutable platform id (userId), not the - // handle (WebWallet._identityKey(platform, userId) since #111). - assertEq( - WebWallet(payable(wallet)).sessionAt(keccak256(abi.encode(PLATFORM, verifier.userId())), 0), - address(0xDECAF) - ); - } - - function test_link_walletMismatch_reverts() public { - // nonce bound to a different address than the caller → front-run guard. - verifier.set("alice@gmail.com", address(0xBEEF), FUTURE); - vm.prank(wallet); - vm.expectRevert(Registry.LinkWalletMismatch.selector); - registry.link_identity_oidc(PLATFORM, hex"", address(0xDECAF)); - } - - function test_link_unregisteredCaller_reverts() public { - address stranger = address(0x5727A6); - verifier.set("alice@gmail.com", stranger, FUTURE); - vm.prank(stranger); - vm.expectRevert(Registry.NotRegisteredWallet.selector); - registry.link_identity_oidc(PLATFORM, hex"", address(0xDECAF)); - } - - function test_link_idAlreadyLinkedToOtherWallet_reverts() public { - // wallet links alice's id. - verifier.set("alice@gmail.com", wallet, FUTURE); - vm.prank(wallet); - registry.link_identity_oidc(PLATFORM, hex"", address(0xDECAF)); - - // Stand up a second registered wallet. - verifier.set("second@gmail.com", address(0xBEEF11), FUTURE); - registry.register_session_oidc(PLATFORM, hex""); - address wallet2 = registry.resolve(PLATFORM, "second@gmail.com"); - assertTrue(wallet2 != address(0) && wallet2 != wallet); - - // wallet2 tries to link alice's id (already owned by `wallet`) → conflict. - verifier.set("alice@gmail.com", wallet2, FUTURE); - vm.prank(wallet2); - vm.expectRevert(Registry.IdentityAlreadyLinked.selector); - registry.link_identity_oidc(PLATFORM, hex"", address(0xFADE)); - } - - function test_link_unsetPlatform_reverts() public { - verifier.set("alice@gmail.com", wallet, FUTURE); - vm.prank(wallet); - vm.expectRevert(Registry.OidcVerifierNotSet.selector); - registry.link_identity_oidc("not.configured", hex"", address(0xDECAF)); - } - - function test_link_expiredJwt_reverts() public { - vm.warp(1_000_000); - // expiresAt + CLOCK_SKEW_GRACE <= now → JwtExpired. - verifier.set("alice@gmail.com", wallet, 1); - vm.prank(wallet); - vm.expectRevert(Registry.JwtExpired.selector); - registry.link_identity_oidc(PLATFORM, hex"", address(0xDECAF)); - } -} diff --git a/solidity/archived/login/zk/IZkSessionVerifier.sol b/solidity/archived/login/zk/IZkSessionVerifier.sol deleted file mode 100644 index 70a17478..00000000 --- a/solidity/archived/login/zk/IZkSessionVerifier.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// Plug-in ZK identity-attestation module for the `Registry`. -/// -/// One implementation per platform (X, GitHub-via-JWT, TikTok, …). The -/// verifier owns the platform-specific trust anchors (notary key, OAuth -/// client_id, expected request line) and the matching UltraHonk verifier -/// address. Registry stays small — it dispatches by `platform`, lets the -/// verifier validate the proof, and writes the resulting identity → -/// wallet mapping. -/// -/// The return tuple includes a `nullifier` so the Registry can prevent a -/// stolen bearer from being used twice with different session keys — -/// TLSN-based flows must enforce this explicitly (OIDC-based flows get it -/// for free from JWT `exp` + fresh session keys). -interface IZkSessionVerifier { - /// Verify a ZK proof + notary attestations and return the validated - /// identity binding. - /// - /// `proof` is platform-specific ABI-encoded bytes — the verifier - /// owns the codec. Returns: - /// * `handle` — human-readable identity from the proof (e.g. - /// X username). Registry uses it directly as the - /// handle in its `(platform, handle) → wallet` map. - /// * `sessionKey` — Ethereum address the user is binding to. - /// * `expiresAt` — proof validity deadline (unix seconds). - /// * `nullifier` — domain-separated commitment to the OAuth bearer. - /// Registry checks `usedBearerNullifiers[nullifier]` - /// and stores it, blocking bearer-theft → - /// handle-takeover when the same bearer is - /// presented twice with different session keys. - /// Returns wallet binding committed in the proof's public inputs. - /// Registry enforces semantics: `address(0)` for `register_session_zk`, - /// `msg.sender` for `link_identity_zk`. - /// * `userId` — immutable platform user-id from the same proof - /// ("" when unrevealed → handle-key fallback). Registry - /// keys identity by `(platform, userId)` when non-empty. - function verifyAndExtract(bytes calldata proof) - external - view - returns ( - string memory handle, - address sessionKey, - address walletAddress, - uint256 expiresAt, - bytes32 nullifier, - string memory userId - ); - - /// Human-readable platform name (e.g. `"api.x.com"`). Used for events - /// and debugging. - function platformName() external view returns (string memory); -} diff --git a/solidity/archived/login/zk/XHonkVerifier.sol b/solidity/archived/login/zk/XHonkVerifier.sol deleted file mode 100644 index 545cdb96..00000000 --- a/solidity/archived/login/zk/XHonkVerifier.sol +++ /dev/null @@ -1,2461 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Aztec -pragma solidity >=0.8.21; - -uint256 constant N = 131072; -uint256 constant LOG_N = 17; -uint256 constant NUMBER_OF_PUBLIC_INPUTS = 144; -uint256 constant VK_HASH = 0x0ef6521315935086e2161c8343e6e402c97e16770e1db9d53955fe1ca8f86e67; - -library HonkVerificationKey { - function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { - Honk.VerificationKey memory vk = Honk.VerificationKey({ - circuitSize: uint256(131072), - logCircuitSize: uint256(17), - publicInputsSize: uint256(144), - ql: Honk.G1Point({ - x: uint256(0x0867889e76e121ff0ea46eb576fcdb9c764fdd115b01afa7e87b4e3099a2d5ce), - y: uint256(0x07d5b79455c10ea61469a2e382bd963a2f3d67153f0d82e4baee4ef6292becca) - }), - qr: Honk.G1Point({ - x: uint256(0x1715640053f18abe9dafeffc0d5bf5a80cb94eea47f5151555d1e67196676a48), - y: uint256(0x0be32585b8dacfb89120c4484d930de40f141371c75349c7734c4e471c7eddef) - }), - qo: Honk.G1Point({ - x: uint256(0x21ac9f103b1060f6776c5c2b93187360a43079337274e56941ed1ab451142c0a), - y: uint256(0x05647644458128f9509002290b192eb4332e81b00e4bf3bb3f4a83d354c7457a) - }), - q4: Honk.G1Point({ - x: uint256(0x2e7705b2249a201426c5f0dc076eed5f558c60d3ac0c5106f0aaf6d8d03e0e75), - y: uint256(0x0c942280f10309799f8cd9551f484e443bc915bc6c8462d59a21d2211f091cc1) - }), - qm: Honk.G1Point({ - x: uint256(0x2ea756acd0551fbd2a3575f5dc7d014b3b540a19e8a5f28ce8d984db79dfb1b1), - y: uint256(0x25d81ad5e534eebfc6615185e8d96829e9d1758f845758b3013c7f8772cea266) - }), - qc: Honk.G1Point({ - x: uint256(0x0888b3e3d7970e927d86475e7acf0c9c58dfe02aa63ca893ce2ec445a58043af), - y: uint256(0x09771d4d862332c7950d6e3ca05505dadb29a19fda0802ace1a9e8a6b57f1911) - }), - qLookup: Honk.G1Point({ - x: uint256(0x176962206a3bada5864d88e7cb7963c6226a9a49f74960944b1e5ccb2c4d32f8), - y: uint256(0x2fc0e8090751c5c4c66adec65200f5170dbc1f35f5f0a841207104c69694baa4) - }), - qArith: Honk.G1Point({ - x: uint256(0x00f3b169567705eaae7f55fca77b4730bda0b33e30bf821a0df03ecec3ea2da0), - y: uint256(0x0a7cc4a834c095b42672cc204a50f96cdbb6acd0f0734b1511555f64eba98b64) - }), - qDeltaRange: Honk.G1Point({ - x: uint256(0x0ff6bcebe0d10a75dc162bf2c493cb826fe6a763b53fe105956909b275b788f6), - y: uint256(0x04907b4d2ffa96283dc3d84540b5d43da0cfac86c9b0a9e593b9e89d28581417) - }), - qElliptic: Honk.G1Point({ - x: uint256(0x25156f1a49a5638fd864b71e81699df764ebb5613a54d870865534c68a1ef931), - y: uint256(0x0568de5ea5bb65f2673e4e6d50654820e0ad03a4dd508e3e2ef996289b4e579c) - }), - qMemory: Honk.G1Point({ - x: uint256(0x1fd96610c3eba1a291d75d93604b466974e732390ae9c43c4d07d9e2111f3280), - y: uint256(0x166484296c985b88dc75aa05b4a827325d43ada70d79b06477caa4054cf843f1) - }), - qNnf: Honk.G1Point({ - x: uint256(0x28f202265512dde7a5a697d52ed54fc10c7601836a3b3a490afacf8a140fd40a), - y: uint256(0x2d05e6f99b6988423f69c308148b707b067377fe899e028ce73137b0cbf80657) - }), - qPoseidon2External: Honk.G1Point({ - x: uint256(0x18a209b92eb5368292daa81262ced374f49bdb1dbc2a371d402d1327a7991513), - y: uint256(0x09ec706b3f521933afb919beeb32e090a25145ebcea055dd069b3b1878561f84) - }), - qPoseidon2Internal: Honk.G1Point({ - x: uint256(0x2693828b993cb5ff8161abbc576a91e80f505919f95da7bff495288ea76fd89d), - y: uint256(0x039c4f79c2bb9a1a185f35694fb80eae4307f97dc9dd58b062ce91e539caeb8b) - }), - s1: Honk.G1Point({ - x: uint256(0x264eadf3c9ab93582340c9e06e1735ba606bcf411c027c5d6bbec5f20d51514f), - y: uint256(0x12a802d923f9a9c7359e0095b43c36fd30794193f12878846ace49f811cb0db7) - }), - s2: Honk.G1Point({ - x: uint256(0x21c3a35965ac30ef5388ceb4f70b58366e34ba3d85c97714bb27b72d5ce3fef5), - y: uint256(0x0b3d1c44a295f45bb439f4e3ed9c2b1f9bd8d328f7dcd06a845b1c521f4f5f38) - }), - s3: Honk.G1Point({ - x: uint256(0x1eecc81a55fa8c08c709e243a712e349406ae8b0883409366f3148561690b602), - y: uint256(0x0ba194a1bc5e39c5e5a4c727932dd752fcdedef069e885fe1b7747b29354585d) - }), - s4: Honk.G1Point({ - x: uint256(0x0fb2756602018406076881fd28c7cf3ec5daeb9bc4230534a225333e241389ae), - y: uint256(0x016d223be53bedfae5bf6bf3db97976c9ba245f2ab52ad8aae3b873ec832a95a) - }), - t1: Honk.G1Point({ - x: uint256(0x1c6e54d5ec7c8998c794bcb74f4b05723704102c0d45e77533d65d3b3e678fce), - y: uint256(0x282bd3c84f22fb16a533a5b1bd161a0b70b6e0bcbd708d7e141c49e782c9f0b4) - }), - t2: Honk.G1Point({ - x: uint256(0x10eb2b9935f0821f9dca62a3c429e283b566a2449937709e675779c66c3475fa), - y: uint256(0x0a7d17cc6ff2341ca13bea8c672443ca80223cd46685dbf5e9d168be6b8dc0ca) - }), - t3: Honk.G1Point({ - x: uint256(0x0f4f6ad8247c82205ff8736e08980ff8a0787aa6f9c19abd7cf1b2fe9917862d), - y: uint256(0x305661f3a851ebc1af312aecf40c1d39d2f7c6cb8d736d56322dffc97b83322c) - }), - t4: Honk.G1Point({ - x: uint256(0x2988c1ae84ecafedb9fe29695b3884f727c7240dc6e5991fbff42eea3f42b0f3), - y: uint256(0x2a4a2e89796bf81d6feb9e7678bb0e6f29283d220c0590dc6b7e7bcb2aa6bbdd) - }), - id1: Honk.G1Point({ - x: uint256(0x22ffa5f8e5014ee9926d54f97d50c66c3bad3834b6c4cb3df86582ccc7adf78c), - y: uint256(0x0779b2c4bf48ad4ce2b03a8264cab4a8e284e94b58e0bb2a712ddabafc887bf9) - }), - id2: Honk.G1Point({ - x: uint256(0x197e66ad0062d38f6d7a09181720c639d71d76f8ad69befa54f2362145cafde5), - y: uint256(0x15efc53bb45872c4db79526d05a9c1f86672843c8daadb509d0239034a80c2e0) - }), - id3: Honk.G1Point({ - x: uint256(0x2fe9c6b5c56527700584b279a38fdd94221b359482b3da13cb8707fd639a251c), - y: uint256(0x2084fff46655bf593b12063d6b9af2bf8dac34bea210a4ff6f9da678ad53afad) - }), - id4: Honk.G1Point({ - x: uint256(0x28a6cd231f5d48498bf31c8d0f9fdfe593b8e23fa62b3c70d8178084c42f1a75), - y: uint256(0x27cde83c1c7d331f50a28425d09dc5d05149d4439a5246da1067692d366c6a76) - }), - lagrangeFirst: Honk.G1Point({ - x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), - y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) - }), - lagrangeLast: Honk.G1Point({ - x: uint256(0x06276f4b34f397425ca87ad494829f15cd4486b6868bcade5523892abfb774af), - y: uint256(0x1908ba2901b815796a23c1b26c18757b807a4356819d91a02a6b9479a905d1e9) - }) - }); - return vk; - } -} - -pragma solidity ^0.8.27; - -interface IVerifier { - function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool); -} - -/** - * @notice Library of error codes - * @dev You can run `forge inspect Errors errors` to get the selectors for the optimised verifier - */ -library Errors { - error ValueGeLimbMax(); - error ValueGeGroupOrder(); - error ValueGeFieldOrder(); - - error InvertOfZero(); - error NotPowerOfTwo(); - error ModExpFailed(); - - error ProofLengthWrong(); - error ProofLengthWrongWithLogN(uint256 logN, uint256 actualLength, uint256 expectedLength); - error PublicInputsLengthWrong(); - error SumcheckFailed(); - error ShpleminiFailed(); - - error PointAtInfinity(); - - error ConsistencyCheckFailed(); - error GeminiChallengeInSubgroup(); -} - -type Fr is uint256; - -using {add as +} for Fr global; -using {sub as -} for Fr global; -using {mul as *} for Fr global; - -using {notEqual as !=} for Fr global; -using {equal as ==} for Fr global; - -uint256 constant SUBGROUP_SIZE = 256; -uint256 constant MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617; // Prime field order -uint256 constant P = MODULUS; -Fr constant SUBGROUP_GENERATOR = Fr.wrap(0x07b0c561a6148404f086204a9f36ffb0617942546750f230c893619174a57a76); -Fr constant SUBGROUP_GENERATOR_INVERSE = Fr.wrap(0x204bd3277422fad364751ad938e2b5e6a54cf8c68712848a692c553d0329f5d6); -Fr constant MINUS_ONE = Fr.wrap(MODULUS - 1); -Fr constant ONE = Fr.wrap(1); -Fr constant ZERO = Fr.wrap(0); -// Instantiation - -library FrLib { - bytes4 internal constant FRLIB_MODEXP_FAILED_SELECTOR = 0xf8d61709; - - function invert(Fr value) internal view returns (Fr) { - uint256 v = Fr.unwrap(value); - require(v != 0, Errors.InvertOfZero()); - - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), v) - mstore(add(free, 0x80), sub(MODULUS, 2)) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function pow(Fr base, uint256 v) internal view returns (Fr) { - uint256 b = Fr.unwrap(base); - // Only works for power of 2 - require(v > 0 && (v & (v - 1)) == 0, Errors.NotPowerOfTwo()); - uint256 result; - - // Call the modexp precompile to invert in the field - assembly ("memory-safe") { - let free := mload(0x40) - mstore(free, 0x20) - mstore(add(free, 0x20), 0x20) - mstore(add(free, 0x40), 0x20) - mstore(add(free, 0x60), b) - mstore(add(free, 0x80), v) - mstore(add(free, 0xa0), MODULUS) - let success := staticcall(gas(), 0x05, free, 0xc0, 0x00, 0x20) - if iszero(success) { - mstore(0x00, FRLIB_MODEXP_FAILED_SELECTOR) - revert(0, 0x04) - } - result := mload(0x00) - mstore(0x40, add(free, 0xc0)) - } - - return Fr.wrap(result); - } - - function div(Fr numerator, Fr denominator) internal view returns (Fr) { - unchecked { - return numerator * invert(denominator); - } - } - - function sqr(Fr value) internal pure returns (Fr) { - unchecked { - return value * value; - } - } - - function unwrap(Fr value) internal pure returns (uint256) { - unchecked { - return Fr.unwrap(value); - } - } - - function neg(Fr value) internal pure returns (Fr) { - unchecked { - return Fr.wrap(MODULUS - Fr.unwrap(value)); - } - } - - function from(uint256 value) internal pure returns (Fr) { - unchecked { - require(value < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(value); - } - } - - function fromBytes32(bytes32 value) internal pure returns (Fr) { - unchecked { - uint256 v = uint256(value); - require(v < MODULUS, Errors.ValueGeFieldOrder()); - return Fr.wrap(v); - } - } - - function toBytes32(Fr value) internal pure returns (bytes32) { - unchecked { - return bytes32(Fr.unwrap(value)); - } - } -} - -// Free functions -function add(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function mul(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(mulmod(Fr.unwrap(a), Fr.unwrap(b), MODULUS)); - } -} - -function sub(Fr a, Fr b) pure returns (Fr) { - unchecked { - return Fr.wrap(addmod(Fr.unwrap(a), MODULUS - Fr.unwrap(b), MODULUS)); - } -} - -function notEqual(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) != Fr.unwrap(b); - } -} - -function equal(Fr a, Fr b) pure returns (bool) { - unchecked { - return Fr.unwrap(a) == Fr.unwrap(b); - } -} - -uint256 constant CONST_PROOF_SIZE_LOG_N = 25; - -uint256 constant NUMBER_OF_SUBRELATIONS = 28; -uint256 constant BATCHED_RELATION_PARTIAL_LENGTH = 8; -uint256 constant ZK_BATCHED_RELATION_PARTIAL_LENGTH = 9; -uint256 constant NUMBER_OF_ENTITIES = 41; -// The number of entities added for ZK (gemini_masking_poly) -uint256 constant NUM_MASKING_POLYNOMIALS = 1; -uint256 constant NUMBER_OF_ENTITIES_ZK = NUMBER_OF_ENTITIES + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_UNSHIFTED = 36; -uint256 constant NUMBER_UNSHIFTED_ZK = NUMBER_UNSHIFTED + NUM_MASKING_POLYNOMIALS; -uint256 constant NUMBER_TO_BE_SHIFTED = 5; -uint256 constant PAIRING_POINTS_SIZE = 8; - -uint256 constant FIELD_ELEMENT_SIZE = 0x20; -uint256 constant GROUP_ELEMENT_SIZE = 0x40; - -// Powers of alpha used to batch subrelations (alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)) -uint256 constant NUMBER_OF_ALPHAS = NUMBER_OF_SUBRELATIONS - 1; - -// ENUM FOR WIRES -enum WIRE { - Q_M, - Q_C, - Q_L, - Q_R, - Q_O, - Q_4, - Q_LOOKUP, - Q_ARITH, - Q_RANGE, - Q_ELLIPTIC, - Q_MEMORY, - Q_NNF, - Q_POSEIDON2_EXTERNAL, - Q_POSEIDON2_INTERNAL, - SIGMA_1, - SIGMA_2, - SIGMA_3, - SIGMA_4, - ID_1, - ID_2, - ID_3, - ID_4, - TABLE_1, - TABLE_2, - TABLE_3, - TABLE_4, - LAGRANGE_FIRST, - LAGRANGE_LAST, - W_L, - W_R, - W_O, - W_4, - Z_PERM, - LOOKUP_INVERSES, - LOOKUP_READ_COUNTS, - LOOKUP_READ_TAGS, - W_L_SHIFT, - W_R_SHIFT, - W_O_SHIFT, - W_4_SHIFT, - Z_PERM_SHIFT -} - -library Honk { - struct G1Point { - uint256 x; - uint256 y; - } - - struct VerificationKey { - // Misc Params - uint256 circuitSize; - uint256 logCircuitSize; - uint256 publicInputsSize; - // Selectors - G1Point qm; - G1Point qc; - G1Point ql; - G1Point qr; - G1Point qo; - G1Point q4; - G1Point qLookup; // Lookup - G1Point qArith; // Arithmetic widget - G1Point qDeltaRange; // Delta Range sort - G1Point qMemory; // Memory - G1Point qNnf; // Non-native Field - G1Point qElliptic; // Auxillary - G1Point qPoseidon2External; - G1Point qPoseidon2Internal; - // Copy constraints - G1Point s1; - G1Point s2; - G1Point s3; - G1Point s4; - // Copy identity - G1Point id1; - G1Point id2; - G1Point id3; - G1Point id4; - // Precomputed lookup table - G1Point t1; - G1Point t2; - G1Point t3; - G1Point t4; - // Fixed first and last - G1Point lagrangeFirst; - G1Point lagrangeLast; - } - - struct RelationParameters { - // challenges - Fr eta; - Fr beta; - Fr gamma; - // derived - Fr publicInputsDelta; - } - - struct Proof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // Free wires - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Lookup helpers - Permutations - G1Point zPerm; - // Lookup helpers - logup - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Sumcheck - Fr[BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr[NUMBER_OF_ENTITIES] sumcheckEvaluations; - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - G1Point shplonkQ; - G1Point kzgQuotient; - } - - /// forge-lint: disable-next-item(pascal-case-struct) - struct ZKProof { - // Pairing point object - Fr[PAIRING_POINTS_SIZE] pairingPointObject; - // ZK: Gemini masking polynomial commitment (sent first, right after public inputs) - G1Point geminiMaskingPoly; - // Commitments to wire polynomials - G1Point w1; - G1Point w2; - G1Point w3; - G1Point w4; - // Commitments to logup witness polynomials - G1Point lookupReadCounts; - G1Point lookupReadTags; - G1Point lookupInverses; - // Commitment to grand permutation polynomial - G1Point zPerm; - G1Point[3] libraCommitments; - // Sumcheck - Fr libraSum; - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH][CONST_PROOF_SIZE_LOG_N] sumcheckUnivariates; - Fr libraEvaluation; - Fr[NUMBER_OF_ENTITIES_ZK] sumcheckEvaluations; // Includes gemini_masking_poly eval at index 0 (first position) - // Shplemini - G1Point[CONST_PROOF_SIZE_LOG_N - 1] geminiFoldComms; - Fr[CONST_PROOF_SIZE_LOG_N] geminiAEvaluations; - Fr[4] libraPolyEvals; - G1Point shplonkQ; - G1Point kzgQuotient; - } -} - -// ZKTranscript library to generate fiat shamir challenges, the ZK transcript only differest -/// forge-lint: disable-next-item(pascal-case-struct) -struct ZKTranscript { - // Oink - Honk.RelationParameters relationParameters; - Fr[NUMBER_OF_ALPHAS] alphas; // Powers of alpha: [alpha, alpha^2, ..., alpha^(NUM_SUBRELATIONS-1)] - Fr[CONST_PROOF_SIZE_LOG_N] gateChallenges; - // Sumcheck - Fr libraChallenge; - Fr[CONST_PROOF_SIZE_LOG_N] sumCheckUChallenges; - // Shplemini - Fr rho; - Fr geminiR; - Fr shplonkNu; - Fr shplonkZ; - // Derived - Fr publicInputsDelta; -} - -library ZKTranscriptLib { - function generateTranscript( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - uint256 logN - ) external pure returns (ZKTranscript memory t) { - Fr previousChallenge; - (t.relationParameters, previousChallenge) = - generateRelationParametersChallenges(proof, publicInputs, vkHash, publicInputsSize, previousChallenge); - - (t.alphas, previousChallenge) = generateAlphaChallenges(previousChallenge, proof); - - (t.gateChallenges, previousChallenge) = generateGateChallenges(previousChallenge, logN); - (t.libraChallenge, previousChallenge) = generateLibraChallenge(previousChallenge, proof); - (t.sumCheckUChallenges, previousChallenge) = generateSumcheckChallenges(proof, previousChallenge, logN); - - (t.rho, previousChallenge) = generateRhoChallenge(proof, previousChallenge); - - (t.geminiR, previousChallenge) = generateGeminiRChallenge(proof, previousChallenge, logN); - - (t.shplonkNu, previousChallenge) = generateShplonkNuChallenge(proof, previousChallenge, logN); - - (t.shplonkZ, previousChallenge) = generateShplonkZChallenge(proof, previousChallenge); - return t; - } - - function splitChallenge(Fr challenge) internal pure returns (Fr first, Fr second) { - uint256 challengeU256 = uint256(Fr.unwrap(challenge)); - // Split into two equal 127-bit chunks (254/2) - uint256 lo = challengeU256 & 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 127 bits - uint256 hi = challengeU256 >> 127; - first = FrLib.from(lo); - second = FrLib.from(hi); - } - - function generateRelationParametersChallenges( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize, - Fr previousChallenge - ) internal pure returns (Honk.RelationParameters memory rp, Fr nextPreviousChallenge) { - (rp.eta, previousChallenge) = generateEtaChallenge(proof, publicInputs, vkHash, publicInputsSize); - - (rp.beta, rp.gamma, nextPreviousChallenge) = generateBetaGammaChallenges(previousChallenge, proof); - } - - function generateEtaChallenge( - Honk.ZKProof memory proof, - bytes32[] calldata publicInputs, - uint256 vkHash, - uint256 publicInputsSize - ) internal pure returns (Fr eta, Fr previousChallenge) { - // Size: 1 (vkHash) + publicInputsSize + 8 (geminiMask(2) + 3 wires(6)) - bytes32[] memory round0 = new bytes32[](1 + publicInputsSize + 8); - round0[0] = bytes32(vkHash); - - for (uint256 i = 0; i < publicInputsSize - PAIRING_POINTS_SIZE; i++) { - require(uint256(publicInputs[i]) < P, Errors.ValueGeFieldOrder()); - round0[1 + i] = publicInputs[i]; - } - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - round0[1 + publicInputsSize - PAIRING_POINTS_SIZE + i] = FrLib.toBytes32(proof.pairingPointObject[i]); - } - - // For ZK flavors: hash the gemini masking poly commitment (sent right after public inputs) - round0[1 + publicInputsSize] = bytes32(proof.geminiMaskingPoly.x); - round0[1 + publicInputsSize + 1] = bytes32(proof.geminiMaskingPoly.y); - - // Create the first challenge - // Note: w4 is added to the challenge later on - round0[1 + publicInputsSize + 2] = bytes32(proof.w1.x); - round0[1 + publicInputsSize + 3] = bytes32(proof.w1.y); - round0[1 + publicInputsSize + 4] = bytes32(proof.w2.x); - round0[1 + publicInputsSize + 5] = bytes32(proof.w2.y); - round0[1 + publicInputsSize + 6] = bytes32(proof.w3.x); - round0[1 + publicInputsSize + 7] = bytes32(proof.w3.y); - - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round0))) % P); - (eta,) = splitChallenge(previousChallenge); - } - - function generateBetaGammaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr beta, Fr gamma, Fr nextPreviousChallenge) - { - bytes32[7] memory round1; - round1[0] = FrLib.toBytes32(previousChallenge); - round1[1] = bytes32(proof.lookupReadCounts.x); - round1[2] = bytes32(proof.lookupReadCounts.y); - round1[3] = bytes32(proof.lookupReadTags.x); - round1[4] = bytes32(proof.lookupReadTags.y); - round1[5] = bytes32(proof.w4.x); - round1[6] = bytes32(proof.w4.y); - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(round1))) % P); - (beta, gamma) = splitChallenge(nextPreviousChallenge); - } - - // Alpha challenges non-linearise the gate contributions - function generateAlphaChallenges(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr[NUMBER_OF_ALPHAS] memory alphas, Fr nextPreviousChallenge) - { - // Generate the original sumcheck alpha 0 by hashing zPerm and zLookup - uint256[5] memory alpha0; - alpha0[0] = Fr.unwrap(previousChallenge); - alpha0[1] = proof.lookupInverses.x; - alpha0[2] = proof.lookupInverses.y; - alpha0[3] = proof.zPerm.x; - alpha0[4] = proof.zPerm.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(alpha0))) % P); - Fr alpha; - (alpha,) = splitChallenge(nextPreviousChallenge); - - // Compute powers of alpha for batching subrelations - alphas[0] = alpha; - for (uint256 i = 1; i < NUMBER_OF_ALPHAS; i++) { - alphas[i] = alphas[i - 1] * alpha; - } - } - - function generateGateChallenges(Fr previousChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory gateChallenges, Fr nextPreviousChallenge) - { - previousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(Fr.unwrap(previousChallenge)))) % P); - (gateChallenges[0],) = splitChallenge(previousChallenge); - for (uint256 i = 1; i < logN; i++) { - gateChallenges[i] = gateChallenges[i - 1] * gateChallenges[i - 1]; - } - nextPreviousChallenge = previousChallenge; - } - - function generateLibraChallenge(Fr previousChallenge, Honk.ZKProof memory proof) - internal - pure - returns (Fr libraChallenge, Fr nextPreviousChallenge) - { - // 2 comm, 1 sum, 1 challenge - uint256[4] memory challengeData; - challengeData[0] = Fr.unwrap(previousChallenge); - challengeData[1] = proof.libraCommitments[0].x; - challengeData[2] = proof.libraCommitments[0].y; - challengeData[3] = Fr.unwrap(proof.libraSum); - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(challengeData))) % P); - (libraChallenge,) = splitChallenge(nextPreviousChallenge); - } - - function generateSumcheckChallenges(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckChallenges, Fr nextPreviousChallenge) - { - for (uint256 i = 0; i < logN; i++) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH + 1] memory univariateChal; - univariateChal[0] = prevChallenge; - - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - univariateChal[j + 1] = proof.sumcheckUnivariates[i][j]; - } - prevChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(univariateChal))) % P); - - (sumcheckChallenges[i],) = splitChallenge(prevChallenge); - } - nextPreviousChallenge = prevChallenge; - } - - // We add Libra claimed eval + 2 libra commitments (grand_sum, quotient) - function generateRhoChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr rho, Fr nextPreviousChallenge) - { - uint256[NUMBER_OF_ENTITIES_ZK + 6] memory rhoChallengeElements; - rhoChallengeElements[0] = Fr.unwrap(prevChallenge); - uint256 i; - for (i = 1; i <= NUMBER_OF_ENTITIES_ZK; i++) { - rhoChallengeElements[i] = Fr.unwrap(proof.sumcheckEvaluations[i - 1]); - } - rhoChallengeElements[i] = Fr.unwrap(proof.libraEvaluation); - i += 1; - rhoChallengeElements[i] = proof.libraCommitments[1].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[1].y; - i += 2; - rhoChallengeElements[i] = proof.libraCommitments[2].x; - rhoChallengeElements[i + 1] = proof.libraCommitments[2].y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(rhoChallengeElements))) % P); - (rho,) = splitChallenge(nextPreviousChallenge); - } - - function generateGeminiRChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr geminiR, Fr nextPreviousChallenge) - { - uint256[] memory gR = new uint256[]((logN - 1) * 2 + 1); - gR[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 0; i < logN - 1; i++) { - gR[1 + i * 2] = proof.geminiFoldComms[i].x; - gR[2 + i * 2] = proof.geminiFoldComms[i].y; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(gR))) % P); - - (geminiR,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkNuChallenge(Honk.ZKProof memory proof, Fr prevChallenge, uint256 logN) - internal - pure - returns (Fr shplonkNu, Fr nextPreviousChallenge) - { - uint256[] memory shplonkNuChallengeElements = new uint256[](logN + 1 + 4); - shplonkNuChallengeElements[0] = Fr.unwrap(prevChallenge); - - for (uint256 i = 1; i <= logN; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.geminiAEvaluations[i - 1]); - } - - uint256 libraIdx = 0; - for (uint256 i = logN + 1; i <= logN + 4; i++) { - shplonkNuChallengeElements[i] = Fr.unwrap(proof.libraPolyEvals[libraIdx]); - libraIdx++; - } - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkNuChallengeElements))) % P); - (shplonkNu,) = splitChallenge(nextPreviousChallenge); - } - - function generateShplonkZChallenge(Honk.ZKProof memory proof, Fr prevChallenge) - internal - pure - returns (Fr shplonkZ, Fr nextPreviousChallenge) - { - uint256[3] memory shplonkZChallengeElements; - shplonkZChallengeElements[0] = Fr.unwrap(prevChallenge); - - shplonkZChallengeElements[1] = proof.shplonkQ.x; - shplonkZChallengeElements[2] = proof.shplonkQ.y; - - nextPreviousChallenge = FrLib.from(uint256(keccak256(abi.encodePacked(shplonkZChallengeElements))) % P); - (shplonkZ,) = splitChallenge(nextPreviousChallenge); - } - - function loadProof(bytes calldata proof, uint256 logN) internal pure returns (Honk.ZKProof memory p) { - uint256 boundary = 0x0; - - // Pairing point object - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - uint256 limb = uint256(bytes32(proof[boundary:boundary + FIELD_ELEMENT_SIZE])); - // lo limbs (even index) < 2^136, hi limbs (odd index) < 2^120 - require(limb < 2 ** (i % 2 == 0 ? 136 : 120), Errors.ValueGeLimbMax()); - p.pairingPointObject[i] = FrLib.from(limb); - boundary += FIELD_ELEMENT_SIZE; - } - - // Gemini masking polynomial commitment (sent first in ZK flavors, right after pairing points) - p.geminiMaskingPoly = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Commitments - p.w1 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w2 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w3 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Lookup / Permutation Helper Commitments - p.lookupReadCounts = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupReadTags = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.w4 = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.lookupInverses = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.zPerm = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[0] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - p.libraSum = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - // Sumcheck univariates - for (uint256 i = 0; i < logN; i++) { - for (uint256 j = 0; j < ZK_BATCHED_RELATION_PARTIAL_LENGTH; j++) { - p.sumcheckUnivariates[i][j] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - } - - // Sumcheck evaluations (includes gemini_masking_poly eval at index 0 for ZK flavors) - for (uint256 i = 0; i < NUMBER_OF_ENTITIES_ZK; i++) { - p.sumcheckEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - p.libraEvaluation = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - - p.libraCommitments[1] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - p.libraCommitments[2] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - - // Gemini - // Read gemini fold univariates - for (uint256 i = 0; i < logN - 1; i++) { - p.geminiFoldComms[i] = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - } - - // Read gemini a evaluations - for (uint256 i = 0; i < logN; i++) { - p.geminiAEvaluations[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - for (uint256 i = 0; i < 4; i++) { - p.libraPolyEvals[i] = bytesToFr(proof[boundary:boundary + FIELD_ELEMENT_SIZE]); - boundary += FIELD_ELEMENT_SIZE; - } - - // Shplonk - p.shplonkQ = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - boundary += GROUP_ELEMENT_SIZE; - // KZG - p.kzgQuotient = bytesToG1Point(proof[boundary:boundary + GROUP_ELEMENT_SIZE]); - } -} - -library RelationsLib { - struct EllipticParams { - // Points - Fr x_1; - Fr y_1; - Fr x_2; - Fr y_2; - Fr y_3; - Fr x_3; - // push accumulators into memory - Fr x_double_identity; - } - - // Parameters used within the Memory Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct MemParams { - Fr memory_record_check; - Fr partial_record_check; - Fr next_gate_access_type; - Fr record_delta; - Fr index_delta; - Fr adjacent_values_match_if_adjacent_indices_match; - Fr adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation; - Fr access_check; - Fr next_gate_access_type_is_boolean; - Fr ROM_consistency_check_identity; - Fr RAM_consistency_check_identity; - Fr timestamp_delta; - Fr RAM_timestamp_check_identity; - Fr memory_identity; - Fr index_is_monotonically_increasing; - } - - // Parameters used within the Non-Native Field Relation - // A struct is used to work around stack too deep. This relation has alot of variables - struct NnfParams { - Fr limb_subproduct; - Fr non_native_field_gate_1; - Fr non_native_field_gate_2; - Fr non_native_field_gate_3; - Fr limb_accumulator_1; - Fr limb_accumulator_2; - Fr nnf_identity; - } - - struct PoseidonExternalParams { - Fr s1; - Fr s2; - Fr s3; - Fr s4; - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr t0; - Fr t1; - Fr t2; - Fr t3; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr q_pos_by_scaling; - } - - struct PoseidonInternalParams { - Fr u1; - Fr u2; - Fr u3; - Fr u4; - Fr u_sum; - Fr v1; - Fr v2; - Fr v3; - Fr v4; - Fr s1; - Fr q_pos_by_scaling; - } - - Fr internal constant GRUMPKIN_CURVE_B_PARAMETER_NEGATED = Fr.wrap(17); // -(-17) - uint256 internal constant NEG_HALF_MODULO_P = 0x183227397098d014dc2822db40c0ac2e9419f4243cdcb848a1f0fac9f8000000; - - // Constants for the Non-native Field relation - Fr internal constant LIMB_SIZE = Fr.wrap(uint256(1) << 68); - Fr internal constant SUBLIMB_SHIFT = Fr.wrap(uint256(1) << 14); - - function accumulateRelationEvaluations( - Fr[NUMBER_OF_ENTITIES] memory purportedEvaluations, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges, - Fr powPartialEval - ) internal pure returns (Fr accumulator) { - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations; - - // Accumulate all relations in Ultra Honk - each with varying number of subrelations - accumulateArithmeticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePermutationRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateLogDerivativeLookupRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateDeltaRangeRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateEllipticRelation(purportedEvaluations, evaluations, powPartialEval); - accumulateMemoryRelation(purportedEvaluations, rp, evaluations, powPartialEval); - accumulateNnfRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonExternalRelation(purportedEvaluations, evaluations, powPartialEval); - accumulatePoseidonInternalRelation(purportedEvaluations, evaluations, powPartialEval); - - // batch the subrelations with the precomputed alpha powers to obtain the full honk relation - accumulator = scaleAndBatchSubrelations(evaluations, subrelationChallenges); - } - - /** - * Aesthetic helper function that is used to index by enum into proof.sumcheckEvaluations, it avoids - * the relation checking code being cluttered with uint256 type casting, which is often a different colour in code - * editors, and thus is noisy. - */ - function wire(Fr[NUMBER_OF_ENTITIES] memory p, WIRE _wire) internal pure returns (Fr) { - return p[uint256(_wire)]; - } - - /** - * Ultra Arithmetic Relation - * - */ - function accumulateArithmeticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - // Relation 0 - Fr q_arith = wire(p, WIRE.Q_ARITH); - { - Fr neg_half = Fr.wrap(NEG_HALF_MODULO_P); - - Fr accum = (q_arith - Fr.wrap(3)) * (wire(p, WIRE.Q_M) * wire(p, WIRE.W_R) * wire(p, WIRE.W_L)) * neg_half; - accum = accum + (wire(p, WIRE.Q_L) * wire(p, WIRE.W_L)) + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_R)) - + (wire(p, WIRE.Q_O) * wire(p, WIRE.W_O)) + (wire(p, WIRE.Q_4) * wire(p, WIRE.W_4)) + wire(p, WIRE.Q_C); - accum = accum + (q_arith - ONE) * wire(p, WIRE.W_4_SHIFT); - accum = accum * q_arith; - accum = accum * domainSep; - evals[0] = accum; - } - - // Relation 1 - { - Fr accum = wire(p, WIRE.W_L) + wire(p, WIRE.W_4) - wire(p, WIRE.W_L_SHIFT) + wire(p, WIRE.Q_M); - accum = accum * (q_arith - Fr.wrap(2)); - accum = accum * (q_arith - ONE); - accum = accum * q_arith; - accum = accum * domainSep; - evals[1] = accum; - } - } - - function accumulatePermutationRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr grand_product_numerator; - Fr grand_product_denominator; - - { - Fr num = wire(p, WIRE.W_L) + wire(p, WIRE.ID_1) * rp.beta + rp.gamma; - num = num * (wire(p, WIRE.W_R) + wire(p, WIRE.ID_2) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_O) + wire(p, WIRE.ID_3) * rp.beta + rp.gamma); - num = num * (wire(p, WIRE.W_4) + wire(p, WIRE.ID_4) * rp.beta + rp.gamma); - - grand_product_numerator = num; - } - { - Fr den = wire(p, WIRE.W_L) + wire(p, WIRE.SIGMA_1) * rp.beta + rp.gamma; - den = den * (wire(p, WIRE.W_R) + wire(p, WIRE.SIGMA_2) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_O) + wire(p, WIRE.SIGMA_3) * rp.beta + rp.gamma); - den = den * (wire(p, WIRE.W_4) + wire(p, WIRE.SIGMA_4) * rp.beta + rp.gamma); - - grand_product_denominator = den; - } - - // Contribution 2 - { - Fr acc = (wire(p, WIRE.Z_PERM) + wire(p, WIRE.LAGRANGE_FIRST)) * grand_product_numerator; - - acc = acc - - ((wire(p, WIRE.Z_PERM_SHIFT) + (wire(p, WIRE.LAGRANGE_LAST) * rp.publicInputsDelta)) - * grand_product_denominator); - acc = acc * domainSep; - evals[2] = acc; - } - - // Contribution 3 - { - Fr acc = (wire(p, WIRE.LAGRANGE_LAST) * wire(p, WIRE.Z_PERM_SHIFT)) * domainSep; - evals[3] = acc; - } - } - - function accumulateLogDerivativeLookupRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr table_term; - Fr lookup_term; - - // Calculate the write term (the table accumulation) - // table_term = table_1 + γ + table_2 * β + table_3 * β² + table_4 * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - table_term = wire(p, WIRE.TABLE_1) + rp.gamma + (wire(p, WIRE.TABLE_2) * rp.beta) - + (wire(p, WIRE.TABLE_3) * beta_sqr) + (wire(p, WIRE.TABLE_4) * beta_sqr * rp.beta); - } - - // Calculate the read term - // lookup_term = derived_entry_1 + γ + derived_entry_2 * β + derived_entry_3 * β² + q_index * β³ - { - Fr beta_sqr = rp.beta * rp.beta; - Fr derived_entry_1 = wire(p, WIRE.W_L) + rp.gamma + (wire(p, WIRE.Q_R) * wire(p, WIRE.W_L_SHIFT)); - Fr derived_entry_2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_M) * wire(p, WIRE.W_R_SHIFT); - Fr derived_entry_3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_C) * wire(p, WIRE.W_O_SHIFT); - - lookup_term = derived_entry_1 + (derived_entry_2 * rp.beta) + (derived_entry_3 * beta_sqr) - + (wire(p, WIRE.Q_O) * beta_sqr * rp.beta); - } - - Fr lookup_inverse = wire(p, WIRE.LOOKUP_INVERSES) * table_term; - Fr table_inverse = wire(p, WIRE.LOOKUP_INVERSES) * lookup_term; - - Fr inverse_exists_xor = wire(p, WIRE.LOOKUP_READ_TAGS) + wire(p, WIRE.Q_LOOKUP) - - (wire(p, WIRE.LOOKUP_READ_TAGS) * wire(p, WIRE.Q_LOOKUP)); - - // Inverse calculated correctly relation - Fr accumulatorNone = lookup_term * table_term * wire(p, WIRE.LOOKUP_INVERSES) - inverse_exists_xor; - accumulatorNone = accumulatorNone * domainSep; - - // Inverse - Fr accumulatorOne = wire(p, WIRE.Q_LOOKUP) * lookup_inverse - wire(p, WIRE.LOOKUP_READ_COUNTS) * table_inverse; - - Fr read_tag = wire(p, WIRE.LOOKUP_READ_TAGS); - - Fr read_tag_boolean_relation = read_tag * read_tag - read_tag; - - evals[4] = accumulatorNone; - evals[5] = accumulatorOne; - evals[6] = read_tag_boolean_relation * domainSep; - } - - function accumulateDeltaRangeRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - Fr minus_one = ZERO - ONE; - Fr minus_two = ZERO - Fr.wrap(2); - Fr minus_three = ZERO - Fr.wrap(3); - - // Compute wire differences - Fr delta_1 = wire(p, WIRE.W_R) - wire(p, WIRE.W_L); - Fr delta_2 = wire(p, WIRE.W_O) - wire(p, WIRE.W_R); - Fr delta_3 = wire(p, WIRE.W_4) - wire(p, WIRE.W_O); - Fr delta_4 = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_4); - - // Contribution 6 - { - Fr acc = delta_1; - acc = acc * (delta_1 + minus_one); - acc = acc * (delta_1 + minus_two); - acc = acc * (delta_1 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[7] = acc; - } - - // Contribution 7 - { - Fr acc = delta_2; - acc = acc * (delta_2 + minus_one); - acc = acc * (delta_2 + minus_two); - acc = acc * (delta_2 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[8] = acc; - } - - // Contribution 8 - { - Fr acc = delta_3; - acc = acc * (delta_3 + minus_one); - acc = acc * (delta_3 + minus_two); - acc = acc * (delta_3 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[9] = acc; - } - - // Contribution 9 - { - Fr acc = delta_4; - acc = acc * (delta_4 + minus_one); - acc = acc * (delta_4 + minus_two); - acc = acc * (delta_4 + minus_three); - acc = acc * wire(p, WIRE.Q_RANGE); - acc = acc * domainSep; - evals[10] = acc; - } - } - - function accumulateEllipticRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - EllipticParams memory ep; - ep.x_1 = wire(p, WIRE.W_R); - ep.y_1 = wire(p, WIRE.W_O); - - ep.x_2 = wire(p, WIRE.W_L_SHIFT); - ep.y_2 = wire(p, WIRE.W_4_SHIFT); - ep.y_3 = wire(p, WIRE.W_O_SHIFT); - ep.x_3 = wire(p, WIRE.W_R_SHIFT); - - Fr q_sign = wire(p, WIRE.Q_L); - Fr q_is_double = wire(p, WIRE.Q_M); - - // Contribution 10 point addition, x-coordinate check - // q_elliptic * (x3 + x2 + x1)(x2 - x1)(x2 - x1) - y2^2 - y1^2 + 2(y2y1)*q_sign = 0 - Fr x_diff = (ep.x_2 - ep.x_1); - Fr y1_sqr = (ep.y_1 * ep.y_1); - { - // Move to top - Fr partialEval = domainSep; - - Fr y2_sqr = (ep.y_2 * ep.y_2); - Fr y1y2 = ep.y_1 * ep.y_2 * q_sign; - Fr x_add_identity = (ep.x_3 + ep.x_2 + ep.x_1); - x_add_identity = x_add_identity * x_diff * x_diff; - x_add_identity = x_add_identity - y2_sqr - y1_sqr + y1y2 + y1y2; - - evals[11] = x_add_identity * partialEval * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 11 point addition, x-coordinate check - // q_elliptic * (q_sign * y1 + y3)(x2 - x1) + (x3 - x1)(y2 - q_sign * y1) = 0 - { - Fr y1_plus_y3 = ep.y_1 + ep.y_3; - Fr y_diff = ep.y_2 * q_sign - ep.y_1; - Fr y_add_identity = y1_plus_y3 * x_diff + (ep.x_3 - ep.x_1) * y_diff; - evals[12] = y_add_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * (ONE - q_is_double); - } - - // Contribution 10 point doubling, x-coordinate check - // (x3 + x1 + x1) (4y1*y1) - 9 * x1 * x1 * x1 * x1 = 0 - // N.B. we're using the equivalence x1*x1*x1 === y1*y1 - curve_b to reduce degree by 1 - { - Fr x_pow_4 = (y1_sqr + GRUMPKIN_CURVE_B_PARAMETER_NEGATED) * ep.x_1; - Fr y1_sqr_mul_4 = y1_sqr + y1_sqr; - y1_sqr_mul_4 = y1_sqr_mul_4 + y1_sqr_mul_4; - Fr x1_pow_4_mul_9 = x_pow_4 * Fr.wrap(9); - - // NOTE: pushed into memory (stack >:'( ) - ep.x_double_identity = (ep.x_3 + ep.x_1 + ep.x_1) * y1_sqr_mul_4 - x1_pow_4_mul_9; - - Fr acc = ep.x_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - evals[11] = evals[11] + acc; - } - - // Contribution 11 point doubling, y-coordinate check - // (y1 + y1) (2y1) - (3 * x1 * x1)(x1 - x3) = 0 - { - Fr x1_sqr_mul_3 = (ep.x_1 + ep.x_1 + ep.x_1) * ep.x_1; - Fr y_double_identity = x1_sqr_mul_3 * (ep.x_1 - ep.x_3) - (ep.y_1 + ep.y_1) * (ep.y_1 + ep.y_3); - evals[12] = evals[12] + y_double_identity * domainSep * wire(p, WIRE.Q_ELLIPTIC) * q_is_double; - } - } - - function accumulateMemoryRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Honk.RelationParameters memory rp, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - MemParams memory ap; - - // Compute eta powers locally - Fr eta_two = rp.eta * rp.eta; - Fr eta_three = eta_two * rp.eta; - - /** - * MEMORY - * - * A RAM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * t: `timestamp` of memory cell being accessed (used for RAM, set to 0 for ROM) - * * v: `value` of memory cell being accessed - * * a: `access` type of record. read: 0 = read, 1 = write - * * r: `record` of memory cell. record = access + index * eta + timestamp * eta_two + value * eta_three - * - * A ROM memory record contains a tuple of the following fields: - * * i: `index` of memory cell being accessed - * * v: `value1` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * v2:`value2` of memory cell being accessed (ROM tables can store up to 2 values per index) - * * r: `record` of memory cell. record = index * eta + value2 * eta_two + value1 * eta_three - * - * When performing a read/write access, the values of i, t, v, v2, a, r are stored in the following wires + - * selectors, depending on whether the gate is a RAM read/write or a ROM read - * - * | gate type | i | v2/t | v | a | r | - * | --------- | -- | ----- | -- | -- | -- | - * | ROM | w1 | w2 | w3 | -- | w4 | - * | RAM | w1 | w2 | w3 | qc | w4 | - * - * (for accesses where `index` is a circuit constant, it is assumed the circuit will apply a copy constraint on - * `w2` to fix its value) - * - * - */ - - /** - * Memory Record Check - * Partial degree: 1 - * Total degree: 4 - * - * A ROM/ROM access gate can be evaluated with the identity: - * - * qc + w1 \eta + w2 \eta_two + w3 \eta_three - w4 = 0 - * - * For ROM gates, qc = 0 - */ - ap.memory_record_check = wire(p, WIRE.W_O) * eta_three; - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_R) * eta_two); - ap.memory_record_check = ap.memory_record_check + (wire(p, WIRE.W_L) * rp.eta); - ap.memory_record_check = ap.memory_record_check + wire(p, WIRE.Q_C); - ap.partial_record_check = ap.memory_record_check; // used in RAM consistency check; deg 1 or 4 - ap.memory_record_check = ap.memory_record_check - wire(p, WIRE.W_4); - - /** - * Contribution 13 & 14 - * ROM Consistency Check - * Partial degree: 1 - * Total degree: 4 - * - * For every ROM read, a set equivalence check is applied between the record witnesses, and a second set of - * records that are sorted. - * - * We apply the following checks for the sorted records: - * - * 1. w1, w2, w3 correctly map to 'index', 'v1, 'v2' for a given record value at w4 - * 2. index values for adjacent records are monotonically increasing - * 3. if, at gate i, index_i == index_{i + 1}, then value1_i == value1_{i + 1} and value2_i == value2_{i + 1} - * - */ - ap.index_delta = wire(p, WIRE.W_L_SHIFT) - wire(p, WIRE.W_L); - ap.record_delta = wire(p, WIRE.W_4_SHIFT) - wire(p, WIRE.W_4); - - ap.index_is_monotonically_increasing = ap.index_delta * (ap.index_delta - Fr.wrap(1)); // deg 2 - - ap.adjacent_values_match_if_adjacent_indices_match = (ap.index_delta * MINUS_ONE + ONE) * ap.record_delta; // deg 2 - - evals[14] = ap.adjacent_values_match_if_adjacent_indices_match * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - evals[15] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)) - * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 - - ap.ROM_consistency_check_identity = ap.memory_record_check * (wire(p, WIRE.Q_L) * wire(p, WIRE.Q_R)); // deg 3 or 7 - - /** - * Contributions 15,16,17 - * RAM Consistency Check - * - * The 'access' type of the record is extracted with the expression `w_4 - ap.partial_record_check` - * (i.e. for an honest Prover `w1 * eta + w2 * eta^2 + w3 * eta^3 - w4 = access`. - * This is validated by requiring `access` to be boolean - * - * For two adjacent entries in the sorted list if _both_ - * A) index values match - * B) adjacent access value is 0 (i.e. next gate is a READ) - * then - * C) both values must match. - * The gate boolean check is - * (A && B) => C === !(A && B) || C === !A || !B || C - * - * N.B. it is the responsibility of the circuit writer to ensure that every RAM cell is initialized - * with a WRITE operation. - */ - Fr access_type = (wire(p, WIRE.W_4) - ap.partial_record_check); // will be 0 or 1 for honest Prover; deg 1 or 4 - ap.access_check = access_type * (access_type - Fr.wrap(1)); // check value is 0 or 1; deg 2 or 8 - - // reverse order we could re-use `ap.partial_record_check` 1 - ((w3' * eta + w2') * eta + w1') * eta - // deg 1 or 4 - ap.next_gate_access_type = wire(p, WIRE.W_O_SHIFT) * eta_three; - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_R_SHIFT) * eta_two); - ap.next_gate_access_type = ap.next_gate_access_type + (wire(p, WIRE.W_L_SHIFT) * rp.eta); - ap.next_gate_access_type = wire(p, WIRE.W_4_SHIFT) - ap.next_gate_access_type; - - Fr value_delta = wire(p, WIRE.W_O_SHIFT) - wire(p, WIRE.W_O); - ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation = - (ap.index_delta * MINUS_ONE + ONE) * value_delta * (ap.next_gate_access_type * MINUS_ONE + ONE); // deg 3 or 6 - - // We can't apply the RAM consistency check identity on the final entry in the sorted list (the wires in the - // next gate would make the identity fail). We need to validate that its 'access type' bool is correct. Can't - // do with an arithmetic gate because of the `eta` factors. We need to check that the *next* gate's access - // type is correct, to cover this edge case - // deg 2 or 4 - ap.next_gate_access_type_is_boolean = - ap.next_gate_access_type * ap.next_gate_access_type - ap.next_gate_access_type; - - // Putting it all together... - evals[16] = ap.adjacent_values_match_if_adjacent_indices_match_and_next_access_is_a_read_operation - * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 5 or 8 - evals[17] = ap.index_is_monotonically_increasing * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 - evals[18] = ap.next_gate_access_type_is_boolean * (wire(p, WIRE.Q_O)) * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 6 - - ap.RAM_consistency_check_identity = ap.access_check * (wire(p, WIRE.Q_O)); // deg 3 or 9 - - /** - * RAM Timestamp Consistency Check - * - * | w1 | w2 | w3 | w4 | - * | index | timestamp | timestamp_check | -- | - * - * Let delta_index = index_{i + 1} - index_{i} - * - * Iff delta_index == 0, timestamp_check = timestamp_{i + 1} - timestamp_i - * Else timestamp_check = 0 - */ - ap.timestamp_delta = wire(p, WIRE.W_R_SHIFT) - wire(p, WIRE.W_R); - ap.RAM_timestamp_check_identity = (ap.index_delta * MINUS_ONE + ONE) * ap.timestamp_delta - wire(p, WIRE.W_O); // deg 3 - - /** - * Complete Contribution 12 - * The complete RAM/ROM memory identity - * Partial degree: - */ - ap.memory_identity = ap.ROM_consistency_check_identity; // deg 3 or 6 - ap.memory_identity = - ap.memory_identity + ap.RAM_timestamp_check_identity * (wire(p, WIRE.Q_4) * wire(p, WIRE.Q_L)); // deg 4 - ap.memory_identity = ap.memory_identity + ap.memory_record_check * (wire(p, WIRE.Q_M) * wire(p, WIRE.Q_L)); // deg 3 or 6 - ap.memory_identity = ap.memory_identity + ap.RAM_consistency_check_identity; // deg 3 or 9 - - // (deg 3 or 9) + (deg 4) + (deg 3) - ap.memory_identity = ap.memory_identity * (wire(p, WIRE.Q_MEMORY) * domainSep); // deg 4 or 10 - evals[13] = ap.memory_identity; - } - - function accumulateNnfRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - NnfParams memory ap; - - /** - * Contribution 12 - * Non native field arithmetic gate 2 - * deg 4 - * - * _ _ - * / _ _ _ 14 \ - * q_2 . q_4 | (w_1 . w_2) + (w_1 . w_2) + (w_1 . w_4 + w_2 . w_3 - w_3) . 2 - w_3 - w_4 | - * \_ _/ - * - * - */ - ap.limb_subproduct = wire(p, WIRE.W_L) * wire(p, WIRE.W_R_SHIFT) + wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R); - ap.non_native_field_gate_2 = - (wire(p, WIRE.W_L) * wire(p, WIRE.W_4) + wire(p, WIRE.W_R) * wire(p, WIRE.W_O) - wire(p, WIRE.W_O_SHIFT)); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * LIMB_SIZE; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 - wire(p, WIRE.W_4_SHIFT); - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 + ap.limb_subproduct; - ap.non_native_field_gate_2 = ap.non_native_field_gate_2 * wire(p, WIRE.Q_4); - - ap.limb_subproduct = ap.limb_subproduct * LIMB_SIZE; - ap.limb_subproduct = ap.limb_subproduct + (wire(p, WIRE.W_L_SHIFT) * wire(p, WIRE.W_R_SHIFT)); - ap.non_native_field_gate_1 = ap.limb_subproduct; - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 - (wire(p, WIRE.W_O) + wire(p, WIRE.W_4)); - ap.non_native_field_gate_1 = ap.non_native_field_gate_1 * wire(p, WIRE.Q_O); - - ap.non_native_field_gate_3 = ap.limb_subproduct; - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 + wire(p, WIRE.W_4); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 - (wire(p, WIRE.W_O_SHIFT) + wire(p, WIRE.W_4_SHIFT)); - ap.non_native_field_gate_3 = ap.non_native_field_gate_3 * wire(p, WIRE.Q_M); - - Fr non_native_field_identity = - ap.non_native_field_gate_1 + ap.non_native_field_gate_2 + ap.non_native_field_gate_3; - non_native_field_identity = non_native_field_identity * wire(p, WIRE.Q_R); - - // ((((w2' * 2^14 + w1') * 2^14 + w3) * 2^14 + w2) * 2^14 + w1 - w4) * qm - // deg 2 - ap.limb_accumulator_1 = wire(p, WIRE.W_R_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_O); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_R); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * SUBLIMB_SHIFT; - ap.limb_accumulator_1 = ap.limb_accumulator_1 + wire(p, WIRE.W_L); - ap.limb_accumulator_1 = ap.limb_accumulator_1 - wire(p, WIRE.W_4); - ap.limb_accumulator_1 = ap.limb_accumulator_1 * wire(p, WIRE.Q_4); - - // ((((w3' * 2^14 + w2') * 2^14 + w1') * 2^14 + w4) * 2^14 + w3 - w4') * qm - // deg 2 - ap.limb_accumulator_2 = wire(p, WIRE.W_O_SHIFT) * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_R_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_L_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_4); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * SUBLIMB_SHIFT; - ap.limb_accumulator_2 = ap.limb_accumulator_2 + wire(p, WIRE.W_O); - ap.limb_accumulator_2 = ap.limb_accumulator_2 - wire(p, WIRE.W_4_SHIFT); - ap.limb_accumulator_2 = ap.limb_accumulator_2 * wire(p, WIRE.Q_M); - - Fr limb_accumulator_identity = ap.limb_accumulator_1 + ap.limb_accumulator_2; - limb_accumulator_identity = limb_accumulator_identity * wire(p, WIRE.Q_O); // deg 3 - - ap.nnf_identity = non_native_field_identity + limb_accumulator_identity; - ap.nnf_identity = ap.nnf_identity * (wire(p, WIRE.Q_NNF) * domainSep); - evals[19] = ap.nnf_identity; - } - - function accumulatePoseidonExternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonExternalParams memory ep; - - ep.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - ep.s2 = wire(p, WIRE.W_R) + wire(p, WIRE.Q_R); - ep.s3 = wire(p, WIRE.W_O) + wire(p, WIRE.Q_O); - ep.s4 = wire(p, WIRE.W_4) + wire(p, WIRE.Q_4); - - ep.u1 = ep.s1 * ep.s1 * ep.s1 * ep.s1 * ep.s1; - ep.u2 = ep.s2 * ep.s2 * ep.s2 * ep.s2 * ep.s2; - ep.u3 = ep.s3 * ep.s3 * ep.s3 * ep.s3 * ep.s3; - ep.u4 = ep.s4 * ep.s4 * ep.s4 * ep.s4 * ep.s4; - // matrix mul v = M_E * u with 14 additions - ep.t0 = ep.u1 + ep.u2; // u_1 + u_2 - ep.t1 = ep.u3 + ep.u4; // u_3 + u_4 - ep.t2 = ep.u2 + ep.u2 + ep.t1; // 2u_2 - // ep.t2 += ep.t1; // 2u_2 + u_3 + u_4 - ep.t3 = ep.u4 + ep.u4 + ep.t0; // 2u_4 - // ep.t3 += ep.t0; // u_1 + u_2 + 2u_4 - ep.v4 = ep.t1 + ep.t1; - ep.v4 = ep.v4 + ep.v4 + ep.t3; - // ep.v4 += ep.t3; // u_1 + u_2 + 4u_3 + 6u_4 - ep.v2 = ep.t0 + ep.t0; - ep.v2 = ep.v2 + ep.v2 + ep.t2; - // ep.v2 += ep.t2; // 4u_1 + 6u_2 + u_3 + u_4 - ep.v1 = ep.t3 + ep.v2; // 5u_1 + 7u_2 + u_3 + 3u_4 - ep.v3 = ep.t2 + ep.v4; // u_1 + 3u_2 + 5u_3 + 7u_4 - - ep.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_EXTERNAL) * domainSep; - evals[20] = evals[20] + ep.q_pos_by_scaling * (ep.v1 - wire(p, WIRE.W_L_SHIFT)); - - evals[21] = evals[21] + ep.q_pos_by_scaling * (ep.v2 - wire(p, WIRE.W_R_SHIFT)); - - evals[22] = evals[22] + ep.q_pos_by_scaling * (ep.v3 - wire(p, WIRE.W_O_SHIFT)); - - evals[23] = evals[23] + ep.q_pos_by_scaling * (ep.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - function accumulatePoseidonInternalRelation( - Fr[NUMBER_OF_ENTITIES] memory p, - Fr[NUMBER_OF_SUBRELATIONS] memory evals, - Fr domainSep - ) internal pure { - PoseidonInternalParams memory ip; - - Fr[4] memory INTERNAL_MATRIX_DIAGONAL = [ - FrLib.from(0x10dc6e9c006ea38b04b1e03b4bd9490c0d03f98929ca1d7fb56821fd19d3b6e7), - FrLib.from(0x0c28145b6a44df3e0149b3d0a30b3bb599df9756d4dd9b84a86b38cfb45a740b), - FrLib.from(0x00544b8338791518b2c7645a50392798b21f75bb60e3596170067d00141cac15), - FrLib.from(0x222c01175718386f2e2e82eb122789e352e105a3b8fa852613bc534433ee428b) - ]; - - // add round constants - ip.s1 = wire(p, WIRE.W_L) + wire(p, WIRE.Q_L); - - // apply s-box round - ip.u1 = ip.s1 * ip.s1 * ip.s1 * ip.s1 * ip.s1; - ip.u2 = wire(p, WIRE.W_R); - ip.u3 = wire(p, WIRE.W_O); - ip.u4 = wire(p, WIRE.W_4); - - // matrix mul with v = M_I * u 4 muls and 7 additions - ip.u_sum = ip.u1 + ip.u2 + ip.u3 + ip.u4; - - ip.q_pos_by_scaling = wire(p, WIRE.Q_POSEIDON2_INTERNAL) * domainSep; - - ip.v1 = ip.u1 * INTERNAL_MATRIX_DIAGONAL[0] + ip.u_sum; - evals[24] = evals[24] + ip.q_pos_by_scaling * (ip.v1 - wire(p, WIRE.W_L_SHIFT)); - - ip.v2 = ip.u2 * INTERNAL_MATRIX_DIAGONAL[1] + ip.u_sum; - evals[25] = evals[25] + ip.q_pos_by_scaling * (ip.v2 - wire(p, WIRE.W_R_SHIFT)); - - ip.v3 = ip.u3 * INTERNAL_MATRIX_DIAGONAL[2] + ip.u_sum; - evals[26] = evals[26] + ip.q_pos_by_scaling * (ip.v3 - wire(p, WIRE.W_O_SHIFT)); - - ip.v4 = ip.u4 * INTERNAL_MATRIX_DIAGONAL[3] + ip.u_sum; - evals[27] = evals[27] + ip.q_pos_by_scaling * (ip.v4 - wire(p, WIRE.W_4_SHIFT)); - } - - // Batch subrelation evaluations using precomputed powers of alpha - // First subrelation is implicitly scaled by 1, subsequent ones use powers from the subrelationChallenges array - function scaleAndBatchSubrelations( - Fr[NUMBER_OF_SUBRELATIONS] memory evaluations, - Fr[NUMBER_OF_ALPHAS] memory subrelationChallenges - ) internal pure returns (Fr accumulator) { - accumulator = evaluations[0]; - - for (uint256 i = 1; i < NUMBER_OF_SUBRELATIONS; ++i) { - accumulator = accumulator + evaluations[i] * subrelationChallenges[i - 1]; - } - } -} - -library CommitmentSchemeLib { - using FrLib for Fr; - - // Avoid stack too deep - struct ShpleminiIntermediates { - Fr unshiftedScalar; - Fr shiftedScalar; - Fr unshiftedScalarNeg; - Fr shiftedScalarNeg; - // Scalar to be multiplied by [1]₁ - Fr constantTermAccumulator; - // Accumulator for powers of rho - Fr batchingChallenge; - // Linear combination of multilinear (sumcheck) evaluations and powers of rho - Fr batchedEvaluation; - Fr[4] denominators; - Fr[4] batchingScalars; - // 1/(z - r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr posInvertedDenominator; - // 1/(z + r^{2^i}) for i = 0, ..., logSize, dynamically updated - Fr negInvertedDenominator; - // ν^{2i} * 1/(z - r^{2^i}) - Fr scalingFactorPos; - // ν^{2i+1} * 1/(z + r^{2^i}) - Fr scalingFactorNeg; - // Fold_i(r^{2^i}) reconstructed by Verifier - Fr[] foldPosEvaluations; - } - - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., m-1 - function computeFoldPosEvaluations( - Fr[CONST_PROOF_SIZE_LOG_N] memory sumcheckUChallenges, - Fr batchedEvalAccumulator, - Fr[CONST_PROOF_SIZE_LOG_N] memory geminiEvaluations, - Fr[] memory geminiEvalChallengePowers, - uint256 logSize - ) internal view returns (Fr[] memory) { - Fr[] memory foldPosEvaluations = new Fr[](logSize); - for (uint256 i = logSize; i > 0; --i) { - Fr challengePower = geminiEvalChallengePowers[i - 1]; - Fr u = sumcheckUChallenges[i - 1]; - - Fr batchedEvalRoundAcc = - ((challengePower * batchedEvalAccumulator * Fr.wrap(2)) - geminiEvaluations[i - 1] - * (challengePower * (ONE - u) - u)); - // Divide by the denominator - batchedEvalRoundAcc = batchedEvalRoundAcc * (challengePower * (ONE - u) + u).invert(); - - batchedEvalAccumulator = batchedEvalRoundAcc; - foldPosEvaluations[i - 1] = batchedEvalRoundAcc; - } - return foldPosEvaluations; - } - - function computeSquares(Fr r, uint256 logN) internal pure returns (Fr[] memory) { - Fr[] memory squares = new Fr[](logN); - squares[0] = r; - for (uint256 i = 1; i < logN; ++i) { - squares[i] = squares[i - 1].sqr(); - } - return squares; - } -} - -uint256 constant Q = 21888242871839275222246405745257275088696311157297823662689037894645226208583; // EC group order. F_q - -// Fr utility - -function bytesToFr(bytes calldata proofSection) pure returns (Fr scalar) { - scalar = FrLib.fromBytes32(bytes32(proofSection)); -} - -// EC Point utilities -function bytesToG1Point(bytes calldata proofSection) pure returns (Honk.G1Point memory point) { - uint256 x = uint256(bytes32(proofSection[0x00:0x20])); - uint256 y = uint256(bytes32(proofSection[0x20:0x40])); - require(x < Q && y < Q, Errors.ValueGeGroupOrder()); - - // Reject the point at infinity (0,0). EVM precompiles silently treat (0,0) - // as the identity element, which could zero out commitments. - // On-curve validation (y² = x³ + 3) is handled by the ecAdd/ecMul precompiles - // per EIP-196, so we only need to catch this special case here. - require((x | y) != 0, Errors.PointAtInfinity()); - - point = Honk.G1Point({x: x, y: y}); -} - -function negateInplace(Honk.G1Point memory point) pure returns (Honk.G1Point memory) { - // When y == 0 (order-2 point), negation is the same point. Q - 0 = Q which is >= Q. - if (point.y != 0) { - point.y = Q - point.y; - } - return point; -} - -/** - * Convert the pairing points to G1 points. - * - * The pairing points are serialised as an array of 2 limbs representing two points - * (P0 and P1, used for lhs and rhs of pairing operation). - * - * There are 2 limbs (lo, hi) for each coordinate, so 4 limbs per point, 8 total. - * Layout: [P0.x_lo, P0.x_hi, P0.y_lo, P0.y_hi, P1.x_lo, P1.x_hi, P1.y_lo, P1.y_hi] - * - * @param pairingPoints The pairing points to convert. - * @return lhs P0 point - * @return rhs P1 point - */ -function convertPairingPointsToG1(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) - pure - returns (Honk.G1Point memory lhs, Honk.G1Point memory rhs) -{ - // P0 (lhs): x = lo | (hi << 136) - uint256 lhsX = Fr.unwrap(pairingPoints[0]); - lhsX |= Fr.unwrap(pairingPoints[1]) << 136; - - uint256 lhsY = Fr.unwrap(pairingPoints[2]); - lhsY |= Fr.unwrap(pairingPoints[3]) << 136; - - // P1 (rhs): x = lo | (hi << 136) - uint256 rhsX = Fr.unwrap(pairingPoints[4]); - rhsX |= Fr.unwrap(pairingPoints[5]) << 136; - - uint256 rhsY = Fr.unwrap(pairingPoints[6]); - rhsY |= Fr.unwrap(pairingPoints[7]) << 136; - - // Reconstructed coordinates must be < Q to prevent malleability. - // Without this, two different limb encodings could map to the same curve point - // (via mulmod reduction in on-curve checks) but produce different transcript hashes. - require(lhsX < Q && lhsY < Q && rhsX < Q && rhsY < Q, Errors.ValueGeGroupOrder()); - - lhs.x = lhsX; - lhs.y = lhsY; - rhs.x = rhsX; - rhs.y = rhsY; -} - -/** - * Hash the pairing inputs from the present verification context with those extracted from the public inputs. - * - * @param proofPairingPoints Pairing points from the proof - (public inputs). - * @param accLhs Accumulator point for the left side - result of shplemini. - * @param accRhs Accumulator point for the right side - result of shplemini. - * @return recursionSeparator The recursion separator - generated from hashing the above. - */ -function generateRecursionSeparator( - Fr[PAIRING_POINTS_SIZE] memory proofPairingPoints, - Honk.G1Point memory accLhs, - Honk.G1Point memory accRhs -) pure returns (Fr recursionSeparator) { - // hash the proof aggregated X - // hash the proof aggregated Y - // hash the accum X - // hash the accum Y - - (Honk.G1Point memory proofLhs, Honk.G1Point memory proofRhs) = convertPairingPointsToG1(proofPairingPoints); - - uint256[8] memory recursionSeparatorElements; - - // Proof points - recursionSeparatorElements[0] = proofLhs.x; - recursionSeparatorElements[1] = proofLhs.y; - recursionSeparatorElements[2] = proofRhs.x; - recursionSeparatorElements[3] = proofRhs.y; - - // Accumulator points - recursionSeparatorElements[4] = accLhs.x; - recursionSeparatorElements[5] = accLhs.y; - recursionSeparatorElements[6] = accRhs.x; - recursionSeparatorElements[7] = accRhs.y; - - recursionSeparator = FrLib.from(uint256(keccak256(abi.encodePacked(recursionSeparatorElements))) % P); -} - -/** - * G1 Mul with Separator - * Using the ecAdd and ecMul precompiles - * - * @param basePoint The point to multiply. - * @param other The other point to add. - * @param recursionSeperator The separator to use for the multiplication. - * @return `(recursionSeperator * basePoint) + other`. - */ -function mulWithSeperator(Honk.G1Point memory basePoint, Honk.G1Point memory other, Fr recursionSeperator) - view - returns (Honk.G1Point memory) -{ - Honk.G1Point memory result; - - result = ecMul(recursionSeperator, basePoint); - result = ecAdd(result, other); - - return result; -} - -/** - * G1 Mul - * Takes a Fr value and a G1 point and uses the ecMul precompile to return the result. - * - * @param value The value to multiply the point by. - * @param point The point to multiply. - * @return result The result of the multiplication. - */ -function ecMul(Fr value, Honk.G1Point memory point) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write the point into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | point.x - // free + 0x20| point.y - mstore(free, mload(point)) - mstore(add(free, 0x20), mload(add(point, 0x20))) - // Write the scalar into memory (one 32 byte word) - // Memory layout: - // Address | value - // free + 0x40| value - mstore(add(free, 0x40), value) - - // Call the ecMul precompile, it takes in the following - // [point.x, point.y, scalar], and returns the result back into the free memory location. - let success := staticcall(gas(), 0x07, free, 0x60, free, 0x40) - if iszero(success) { - revert(0, 0) - } - // Copy the result of the multiplication back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x60)) - } - - return result; -} - -/** - * G1 Add - * Takes two G1 points and uses the ecAdd precompile to return the result. - * - * @param lhs The left hand side of the addition. - * @param rhs The right hand side of the addition. - * @return result The result of the addition. - */ -function ecAdd(Honk.G1Point memory lhs, Honk.G1Point memory rhs) view returns (Honk.G1Point memory) { - Honk.G1Point memory result; - - assembly ("memory-safe") { - let free := mload(0x40) - // Write lhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free | lhs.x - // free + 0x20| lhs.y - mstore(free, mload(lhs)) - mstore(add(free, 0x20), mload(add(lhs, 0x20))) - - // Write rhs into memory (two 32 byte words) - // Memory layout: - // Address | value - // free + 0x40| rhs.x - // free + 0x60| rhs.y - mstore(add(free, 0x40), mload(rhs)) - mstore(add(free, 0x60), mload(add(rhs, 0x20))) - - // Call the ecAdd precompile, it takes in the following - // [lhs.x, lhs.y, rhs.x, rhs.y], and returns their addition back into the free memory location. - let success := staticcall(gas(), 0x06, free, 0x80, free, 0x40) - if iszero(success) { revert(0, 0) } - - // Copy the result of the addition back into the result memory location. - // Memory layout: - // Address | value - // result | result.x - // result + 0x20| result.y - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - - mstore(0x40, add(free, 0x80)) - } - - return result; -} - -function rejectPointAtInfinity(Honk.G1Point memory point) pure { - require((point.x | point.y) != 0, Errors.PointAtInfinity()); -} - -/** - * Check if pairing point limbs are all zero (default/infinity). - * Default pairing points indicate no recursive verification occurred. - */ -function arePairingPointsDefault(Fr[PAIRING_POINTS_SIZE] memory pairingPoints) pure returns (bool) { - uint256 acc = 0; - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - acc |= Fr.unwrap(pairingPoints[i]); - } - return acc == 0; -} - -function pairing(Honk.G1Point memory rhs, Honk.G1Point memory lhs) view returns (bool decodedResult) { - bytes memory input = abi.encodePacked( - rhs.x, - rhs.y, - // Fixed G2 point - uint256(0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2), - uint256(0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed), - uint256(0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b), - uint256(0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa), - lhs.x, - lhs.y, - // G2 point from VK - uint256(0x260e01b251f6f1c7e7ff4e580791dee8ea51d87a358e038b4efe30fac09383c1), - uint256(0x0118c4d5b837bcc2bc89b5b398b5974e9f5944073b32078b7e231fec938883b0), - uint256(0x04fc6369f7110fe3d25156c1bb9a72859cf2a04641f99ba4ee413c80da6a5fe4), - uint256(0x22febda3c0c0632a56475b4214e5615e11e6dd3f96e6cea2854a87d4dacc5e55) - ); - - (bool success, bytes memory result) = address(0x08).staticcall(input); - decodedResult = success && abi.decode(result, (bool)); -} - -abstract contract BaseZKHonkVerifier is IVerifier { - using FrLib for Fr; - - struct PairingInputs { - Honk.G1Point P_0; - Honk.G1Point P_1; - } - - struct SmallSubgroupIpaIntermediates { - Fr[SUBGROUP_SIZE] challengePolyLagrange; - Fr challengePolyEval; - Fr lagrangeFirst; - Fr lagrangeLast; - Fr rootPower; - Fr[SUBGROUP_SIZE] denominators; // this has to disappear - Fr diff; - } - - // Constants for proof length calculation (matching UltraKeccakZKFlavor) - uint256 internal constant NUM_WITNESS_ENTITIES = 8 + NUM_MASKING_POLYNOMIALS; - uint256 internal constant NUM_ELEMENTS_COMM = 2; // uint256 elements for curve points - uint256 internal constant NUM_ELEMENTS_FR = 1; // uint256 elements for field elements - uint256 internal constant NUM_LIBRA_EVALUATIONS = 4; // libra evaluations - - uint256 internal constant LIBRA_COMMITMENTS = 3; - uint256 internal constant LIBRA_EVALUATIONS = 4; - uint256 internal constant LIBRA_UNIVARIATES_LENGTH = 9; - - uint256 internal constant SHIFTED_COMMITMENTS_START = 30; - uint256 internal constant PERMUTATION_ARGUMENT_VALUE_SEPARATOR = 1 << 28; - - uint256 internal immutable $N; - uint256 internal immutable $LOG_N; - uint256 internal immutable $VK_HASH; - uint256 internal immutable $NUM_PUBLIC_INPUTS; - uint256 internal immutable $MSMSize; - - constructor(uint256 _N, uint256 _logN, uint256 _vkHash, uint256 _numPublicInputs) { - $N = _N; - $LOG_N = _logN; - $VK_HASH = _vkHash; - $NUM_PUBLIC_INPUTS = _numPublicInputs; - $MSMSize = NUMBER_UNSHIFTED_ZK + _logN + LIBRA_COMMITMENTS + 2; - } - - function verify(bytes calldata proof, bytes32[] calldata publicInputs) - public - view - override - returns (bool verified) - { - // Calculate expected proof size based on $LOG_N - uint256 expectedProofSize = calculateProofSize($LOG_N); - - // Check the received proof is the expected size where each field element is 32 bytes - require( - proof.length == expectedProofSize, Errors.ProofLengthWrongWithLogN($LOG_N, proof.length, expectedProofSize) - ); - - Honk.VerificationKey memory vk = loadVerificationKey(); - Honk.ZKProof memory p = ZKTranscriptLib.loadProof(proof, $LOG_N); - - require(publicInputs.length == vk.publicInputsSize - PAIRING_POINTS_SIZE, Errors.PublicInputsLengthWrong()); - - // Generate the fiat shamir challenges for the whole protocol - ZKTranscript memory t = - ZKTranscriptLib.generateTranscript(p, publicInputs, $VK_HASH, $NUM_PUBLIC_INPUTS, $LOG_N); - - // Derive public input delta - t.relationParameters.publicInputsDelta = computePublicInputDelta( - publicInputs, - p.pairingPointObject, - t.relationParameters.beta, - t.relationParameters.gamma, /*pubInputsOffset=*/ - 1 - ); - - // Sumcheck - require(verifySumcheck(p, t), Errors.SumcheckFailed()); - require(verifyShplemini(p, vk, t), Errors.ShpleminiFailed()); - - verified = true; - } - - function computePublicInputDelta( - bytes32[] memory publicInputs, - Fr[PAIRING_POINTS_SIZE] memory pairingPointObject, - Fr beta, - Fr gamma, - uint256 offset - ) internal view returns (Fr publicInputDelta) { - Fr numerator = Fr.wrap(1); - Fr denominator = Fr.wrap(1); - - Fr numeratorAcc = gamma + (beta * FrLib.from(PERMUTATION_ARGUMENT_VALUE_SEPARATOR + offset)); - Fr denominatorAcc = gamma - (beta * FrLib.from(offset + 1)); - - { - for (uint256 i = 0; i < $NUM_PUBLIC_INPUTS - PAIRING_POINTS_SIZE; i++) { - Fr pubInput = FrLib.fromBytes32(publicInputs[i]); - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - - for (uint256 i = 0; i < PAIRING_POINTS_SIZE; i++) { - Fr pubInput = pairingPointObject[i]; - - numerator = numerator * (numeratorAcc + pubInput); - denominator = denominator * (denominatorAcc + pubInput); - - numeratorAcc = numeratorAcc + beta; - denominatorAcc = denominatorAcc - beta; - } - } - - // Fr delta = numerator / denominator; // TOOO: batch invert later? - publicInputDelta = FrLib.div(numerator, denominator); - } - - function verifySumcheck(Honk.ZKProof memory proof, ZKTranscript memory tp) internal view returns (bool verified) { - Fr roundTargetSum = tp.libraChallenge * proof.libraSum; // default 0 - Fr powPartialEvaluation = Fr.wrap(1); - - // We perform sumcheck reductions over log n rounds ( the multivariate degree ) - for (uint256 round; round < $LOG_N; ++round) { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariate = proof.sumcheckUnivariates[round]; - Fr totalSum = roundUnivariate[0] + roundUnivariate[1]; - require(totalSum == roundTargetSum, Errors.SumcheckFailed()); - - Fr roundChallenge = tp.sumCheckUChallenges[round]; - - // Update the round target for the next rounf - roundTargetSum = computeNextTargetSum(roundUnivariate, roundChallenge); - powPartialEvaluation = - powPartialEvaluation * (Fr.wrap(1) + roundChallenge * (tp.gateChallenges[round] - Fr.wrap(1))); - } - - // Last round - // For ZK flavors: sumcheckEvaluations has 42 elements - // Index 0 is gemini_masking_poly, indices 1-41 are the regular entities used in relations - Fr[NUMBER_OF_ENTITIES] memory relationsEvaluations; - for (uint256 i = 0; i < NUMBER_OF_ENTITIES; i++) { - relationsEvaluations[i] = proof.sumcheckEvaluations[i + NUM_MASKING_POLYNOMIALS]; // Skip gemini_masking_poly at index 0 - } - Fr grandHonkRelationSum = RelationsLib.accumulateRelationEvaluations( - relationsEvaluations, tp.relationParameters, tp.alphas, powPartialEvaluation - ); - - Fr evaluation = Fr.wrap(1); - for (uint256 i = 2; i < $LOG_N; i++) { - evaluation = evaluation * tp.sumCheckUChallenges[i]; - } - - grandHonkRelationSum = - grandHonkRelationSum * (Fr.wrap(1) - evaluation) + proof.libraEvaluation * tp.libraChallenge; - verified = (grandHonkRelationSum == roundTargetSum); - } - - // Return the new target sum for the next sumcheck round - function computeNextTargetSum(Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory roundUnivariates, Fr roundChallenge) - internal - view - returns (Fr targetSum) - { - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory BARYCENTRIC_LAGRANGE_DENOMINATORS = [ - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000000240), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593effffd31), - Fr.wrap(0x00000000000000000000000000000000000000000000000000000000000005a0), - Fr.wrap(0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593efffec51), - Fr.wrap(0x0000000000000000000000000000000000000000000000000000000000009d80) - ]; - - // To compute the next target sum, we evaluate the given univariate at a point u (challenge). - - // Performing Barycentric evaluations - // Compute B(x) - Fr numeratorValue = Fr.wrap(1); - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - numeratorValue = numeratorValue * (roundChallenge - Fr.wrap(i)); - } - - Fr[ZK_BATCHED_RELATION_PARTIAL_LENGTH] memory denominatorInverses; - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - denominatorInverses[i] = FrLib.invert(BARYCENTRIC_LAGRANGE_DENOMINATORS[i] * (roundChallenge - Fr.wrap(i))); - } - - for (uint256 i = 0; i < ZK_BATCHED_RELATION_PARTIAL_LENGTH; ++i) { - targetSum = targetSum + roundUnivariates[i] * denominatorInverses[i]; - } - - // Scale the sum by the value of B(x) - targetSum = targetSum * numeratorValue; - } - - function verifyShplemini(Honk.ZKProof memory proof, Honk.VerificationKey memory vk, ZKTranscript memory tp) - internal - view - returns (bool verified) - { - CommitmentSchemeLib.ShpleminiIntermediates memory mem; // stack - - // - Compute vector (r, r², ... , r²⁽ⁿ⁻¹⁾), where n = log_circuit_size - Fr[] memory powers_of_evaluation_challenge = CommitmentSchemeLib.computeSquares(tp.geminiR, $LOG_N); - // Arrays hold values that will be linearly combined for the gemini and shplonk batch openings - Fr[] memory scalars = new Fr[]($MSMSize); - Honk.G1Point[] memory commitments = new Honk.G1Point[]($MSMSize); - - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[0]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[0]).invert(); - - mem.unshiftedScalar = mem.posInvertedDenominator + (tp.shplonkNu * mem.negInvertedDenominator); - mem.shiftedScalar = - tp.geminiR.invert() * (mem.posInvertedDenominator - (tp.shplonkNu * mem.negInvertedDenominator)); - - scalars[0] = Fr.wrap(1); - commitments[0] = proof.shplonkQ; - - /* Batch multivariate opening claims, shifted and unshifted - * The vector of scalars is populated as follows: - * \f[ - * \left( - * - \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{i+k-1} \times \left(\frac{1}{z-r} + \nu \times \frac{1}{z+r}\right), - * - \rho^{i+k} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right), - * \ldots, - * - \rho^{k+m-1} \times \frac{1}{r} \times \left(\frac{1}{z-r} - \nu \times \frac{1}{z+r}\right) - * \right) - * \f] - * - * The following vector is concatenated to the vector of commitments: - * \f[ - * f_0, \ldots, f_{m-1}, f_{\text{shift}, 0}, \ldots, f_{\text{shift}, k-1} - * \f] - * - * Simultaneously, the evaluation of the multilinear polynomial - * \f[ - * \sum \rho^i \cdot f_i + \sum \rho^{i+k} \cdot f_{\text{shift}, i} - * \f] - * at the challenge point \f$ (u_0,\ldots, u_{n-1}) \f$ is computed. - * - * This approach minimizes the number of iterations over the commitments to multilinear polynomials - * and eliminates the need to store the powers of \f$ \rho \f$. - */ - // For ZK flavors: evaluations array is [gemini_masking_poly, qm, qc, ql, qr, ...] - // Start batching challenge at 1, not rho, to match non-ZK pattern - mem.batchingChallenge = Fr.wrap(1); - mem.batchedEvaluation = Fr.wrap(0); - - mem.unshiftedScalarNeg = mem.unshiftedScalar.neg(); - mem.shiftedScalarNeg = mem.shiftedScalar.neg(); - - // Process all NUMBER_UNSHIFTED_ZK evaluations (includes gemini_masking_poly at index 0) - for (uint256 i = 1; i <= NUMBER_UNSHIFTED_ZK; ++i) { - scalars[i] = mem.unshiftedScalarNeg * mem.batchingChallenge; - mem.batchedEvaluation = mem.batchedEvaluation - + (proof.sumcheckEvaluations[i - NUM_MASKING_POLYNOMIALS] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - // g commitments are accumulated at r - // For each of the to be shifted commitments perform the shift in place by - // adding to the unshifted value. - // We do so, as the values are to be used in batchMul later, and as - // `a * c + b * c = (a + b) * c` this will allow us to reduce memory and compute. - // Applied to w1, w2, w3, w4 and zPerm - for (uint256 i = 0; i < NUMBER_TO_BE_SHIFTED; ++i) { - uint256 scalarOff = i + SHIFTED_COMMITMENTS_START; - uint256 evaluationOff = i + NUMBER_UNSHIFTED_ZK; - - scalars[scalarOff] = scalars[scalarOff] + (mem.shiftedScalarNeg * mem.batchingChallenge); - mem.batchedEvaluation = - mem.batchedEvaluation + (proof.sumcheckEvaluations[evaluationOff] * mem.batchingChallenge); - mem.batchingChallenge = mem.batchingChallenge * tp.rho; - } - - commitments[1] = proof.geminiMaskingPoly; - - commitments[2] = vk.qm; - commitments[3] = vk.qc; - commitments[4] = vk.ql; - commitments[5] = vk.qr; - commitments[6] = vk.qo; - commitments[7] = vk.q4; - commitments[8] = vk.qLookup; - commitments[9] = vk.qArith; - commitments[10] = vk.qDeltaRange; - commitments[11] = vk.qElliptic; - commitments[12] = vk.qMemory; - commitments[13] = vk.qNnf; - commitments[14] = vk.qPoseidon2External; - commitments[15] = vk.qPoseidon2Internal; - commitments[16] = vk.s1; - commitments[17] = vk.s2; - commitments[18] = vk.s3; - commitments[19] = vk.s4; - commitments[20] = vk.id1; - commitments[21] = vk.id2; - commitments[22] = vk.id3; - commitments[23] = vk.id4; - commitments[24] = vk.t1; - commitments[25] = vk.t2; - commitments[26] = vk.t3; - commitments[27] = vk.t4; - commitments[28] = vk.lagrangeFirst; - commitments[29] = vk.lagrangeLast; - - // Accumulate proof points - commitments[30] = proof.w1; - commitments[31] = proof.w2; - commitments[32] = proof.w3; - commitments[33] = proof.w4; - commitments[34] = proof.zPerm; - commitments[35] = proof.lookupInverses; - commitments[36] = proof.lookupReadCounts; - commitments[37] = proof.lookupReadTags; - - /* Batch gemini claims from the prover - * place the commitments to gemini aᵢ to the vector of commitments, compute the contributions from - * aᵢ(−r²ⁱ) for i=1, … , n−1 to the constant term accumulator, add corresponding scalars - * - * 1. Moves the vector - * \f[ - * \left( \text{com}(A_1), \text{com}(A_2), \ldots, \text{com}(A_{n-1}) \right) - * \f] - * to the 'commitments' vector. - * - * 2. Computes the scalars: - * \f[ - * \frac{\nu^{2}}{z + r^2}, \frac{\nu^3}{z + r^4}, \ldots, \frac{\nu^{n-1}}{z + r^{2^{n-1}}} - * \f] - * and places them into the 'scalars' vector. - * - * 3. Accumulates the summands of the constant term: - * \f[ - * \sum_{i=2}^{n-1} \frac{\nu^{i} \cdot A_i(-r^{2^i})}{z + r^{2^i}} - * \f] - * and adds them to the 'constant_term_accumulator'. - */ - - // Add contributions from A₀(r) and A₀(-r) to constant_term_accumulator: - // Compute the evaluations Aₗ(r^{2ˡ}) for l = 0, ..., $LOG_N - 1 - Fr[] memory foldPosEvaluations = CommitmentSchemeLib.computeFoldPosEvaluations( - tp.sumCheckUChallenges, - mem.batchedEvaluation, - proof.geminiAEvaluations, - powers_of_evaluation_challenge, - $LOG_N - ); - - mem.constantTermAccumulator = foldPosEvaluations[0] * mem.posInvertedDenominator; - mem.constantTermAccumulator = - mem.constantTermAccumulator + (proof.geminiAEvaluations[0] * tp.shplonkNu * mem.negInvertedDenominator); - - mem.batchingChallenge = tp.shplonkNu.sqr(); - uint256 boundary = NUMBER_UNSHIFTED_ZK + 1; - - // Compute Shplonk constant term contributions from Aₗ(± r^{2ˡ}) for l = 1, ..., m-1; - // Compute scalar multipliers for each fold commitment - for (uint256 i = 0; i < $LOG_N - 1; ++i) { - bool dummy_round = i >= ($LOG_N - 1); - - if (!dummy_round) { - // Update inverted denominators - mem.posInvertedDenominator = (tp.shplonkZ - powers_of_evaluation_challenge[i + 1]).invert(); - mem.negInvertedDenominator = (tp.shplonkZ + powers_of_evaluation_challenge[i + 1]).invert(); - - // Compute the scalar multipliers for Aₗ(± r^{2ˡ}) and [Aₗ] - mem.scalingFactorPos = mem.batchingChallenge * mem.posInvertedDenominator; - mem.scalingFactorNeg = mem.batchingChallenge * tp.shplonkNu * mem.negInvertedDenominator; - scalars[boundary + i] = mem.scalingFactorNeg.neg() + mem.scalingFactorPos.neg(); - - // Accumulate the const term contribution given by - // v^{2l} * Aₗ(r^{2ˡ}) /(z-r^{2^l}) + v^{2l+1} * Aₗ(-r^{2ˡ}) /(z+ r^{2^l}) - Fr accumContribution = mem.scalingFactorNeg * proof.geminiAEvaluations[i + 1]; - accumContribution = accumContribution + mem.scalingFactorPos * foldPosEvaluations[i + 1]; - mem.constantTermAccumulator = mem.constantTermAccumulator + accumContribution; - } - // Update the running power of v - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu * tp.shplonkNu; - - commitments[boundary + i] = proof.geminiFoldComms[i]; - } - - boundary += $LOG_N - 1; - - // Finalize the batch opening claim - mem.denominators[0] = Fr.wrap(1).div(tp.shplonkZ - tp.geminiR); - mem.denominators[1] = Fr.wrap(1).div(tp.shplonkZ - SUBGROUP_GENERATOR * tp.geminiR); - mem.denominators[2] = mem.denominators[0]; - mem.denominators[3] = mem.denominators[0]; - - for (uint256 i = 0; i < LIBRA_EVALUATIONS; i++) { - Fr scalingFactor = mem.denominators[i] * mem.batchingChallenge; - mem.batchingScalars[i] = scalingFactor.neg(); - mem.batchingChallenge = mem.batchingChallenge * tp.shplonkNu; - mem.constantTermAccumulator = mem.constantTermAccumulator + scalingFactor * proof.libraPolyEvals[i]; - } - scalars[boundary] = mem.batchingScalars[0]; - scalars[boundary + 1] = mem.batchingScalars[1] + mem.batchingScalars[2]; - scalars[boundary + 2] = mem.batchingScalars[3]; - - for (uint256 i = 0; i < LIBRA_COMMITMENTS; i++) { - commitments[boundary++] = proof.libraCommitments[i]; - } - - commitments[boundary] = Honk.G1Point({x: 1, y: 2}); - scalars[boundary++] = mem.constantTermAccumulator; - - require( - checkEvalsConsistency(proof.libraPolyEvals, tp.geminiR, tp.sumCheckUChallenges, proof.libraEvaluation), - Errors.ConsistencyCheckFailed() - ); - - Honk.G1Point memory quotient_commitment = proof.kzgQuotient; - - commitments[boundary] = quotient_commitment; - scalars[boundary] = tp.shplonkZ; // evaluation challenge - - PairingInputs memory pair; - pair.P_0 = batchMul(commitments, scalars); - pair.P_1 = negateInplace(quotient_commitment); - - // Aggregate pairing points (skip if default/infinity — no recursive verification occurred) - if (!arePairingPointsDefault(proof.pairingPointObject)) { - Fr recursionSeparator = generateRecursionSeparator(proof.pairingPointObject, pair.P_0, pair.P_1); - (Honk.G1Point memory P_0_other, Honk.G1Point memory P_1_other) = - convertPairingPointsToG1(proof.pairingPointObject); - - // Validate the points from the proof are on the curve - rejectPointAtInfinity(P_0_other); - rejectPointAtInfinity(P_1_other); - - // accumulate with aggregate points in proof - pair.P_0 = mulWithSeperator(pair.P_0, P_0_other, recursionSeparator); - pair.P_1 = mulWithSeperator(pair.P_1, P_1_other, recursionSeparator); - } - - return pairing(pair.P_0, pair.P_1); - } - - function checkEvalsConsistency( - Fr[LIBRA_EVALUATIONS] memory libraPolyEvals, - Fr geminiR, - Fr[CONST_PROOF_SIZE_LOG_N] memory uChallenges, - Fr libraEval - ) internal view returns (bool check) { - Fr one = Fr.wrap(1); - Fr vanishingPolyEval = geminiR.pow(SUBGROUP_SIZE) - one; - require(vanishingPolyEval != Fr.wrap(0), Errors.GeminiChallengeInSubgroup()); - - SmallSubgroupIpaIntermediates memory mem; - mem.challengePolyLagrange[0] = one; - for (uint256 round = 0; round < $LOG_N; round++) { - uint256 currIdx = 1 + LIBRA_UNIVARIATES_LENGTH * round; - mem.challengePolyLagrange[currIdx] = one; - for (uint256 idx = currIdx + 1; idx < currIdx + LIBRA_UNIVARIATES_LENGTH; idx++) { - mem.challengePolyLagrange[idx] = mem.challengePolyLagrange[idx - 1] * uChallenges[round]; - } - } - - mem.rootPower = one; - mem.challengePolyEval = Fr.wrap(0); - for (uint256 idx = 0; idx < SUBGROUP_SIZE; idx++) { - mem.denominators[idx] = mem.rootPower * geminiR - one; - mem.denominators[idx] = mem.denominators[idx].invert(); - mem.challengePolyEval = mem.challengePolyEval + mem.challengePolyLagrange[idx] * mem.denominators[idx]; - mem.rootPower = mem.rootPower * SUBGROUP_GENERATOR_INVERSE; - } - - Fr numerator = vanishingPolyEval * Fr.wrap(SUBGROUP_SIZE).invert(); - mem.challengePolyEval = mem.challengePolyEval * numerator; - mem.lagrangeFirst = mem.denominators[0] * numerator; - mem.lagrangeLast = mem.denominators[SUBGROUP_SIZE - 1] * numerator; - - mem.diff = mem.lagrangeFirst * libraPolyEvals[2]; - - mem.diff = mem.diff + (geminiR - SUBGROUP_GENERATOR_INVERSE) - * (libraPolyEvals[1] - libraPolyEvals[2] - libraPolyEvals[0] * mem.challengePolyEval); - mem.diff = mem.diff + mem.lagrangeLast * (libraPolyEvals[2] - libraEval) - vanishingPolyEval * libraPolyEvals[3]; - - check = mem.diff == Fr.wrap(0); - } - - // This implementation is the same as above with different constants - function batchMul(Honk.G1Point[] memory base, Fr[] memory scalars) - internal - view - returns (Honk.G1Point memory result) - { - uint256 limit = $MSMSize; - - // Validate all points are on the curve - for (uint256 i = 0; i < limit; ++i) { - rejectPointAtInfinity(base[i]); - } - - bool success = true; - assembly ("memory-safe") { - let free := mload(0x40) - - let count := 0x01 - for {} lt(count, add(limit, 1)) { count := add(count, 1) } { - // Get loop offsets - let base_base := add(base, mul(count, 0x20)) - let scalar_base := add(scalars, mul(count, 0x20)) - - mstore(add(free, 0x40), mload(mload(base_base))) - mstore(add(free, 0x60), mload(add(0x20, mload(base_base)))) - // Add scalar - mstore(add(free, 0x80), mload(scalar_base)) - - success := and(success, staticcall(gas(), 7, add(free, 0x40), 0x60, add(free, 0x40), 0x40)) - // accumulator = accumulator + accumulator_2 - success := and(success, staticcall(gas(), 6, free, 0x80, free, 0x40)) - } - - // Return the result - mstore(result, mload(free)) - mstore(add(result, 0x20), mload(add(free, 0x20))) - } - - require(success, Errors.ShpleminiFailed()); - } - - // Calculate proof size based on log_n (matching UltraKeccakZKFlavor formula) - function calculateProofSize(uint256 logN) internal pure returns (uint256) { - // Witness and Libra commitments - uint256 proofLength = NUM_WITNESS_ENTITIES * NUM_ELEMENTS_COMM; // witness commitments - proofLength += NUM_ELEMENTS_COMM * 3; // Libra concat, grand sum, quotient comms + Gemini masking - - // Sumcheck - proofLength += logN * ZK_BATCHED_RELATION_PARTIAL_LENGTH * NUM_ELEMENTS_FR; // sumcheck univariates - proofLength += NUMBER_OF_ENTITIES_ZK * NUM_ELEMENTS_FR; // sumcheck evaluations - - // Libra and Gemini - proofLength += NUM_ELEMENTS_FR * 2; // Libra sum, claimed eval - proofLength += logN * NUM_ELEMENTS_FR; // Gemini a evaluations - proofLength += NUM_LIBRA_EVALUATIONS * NUM_ELEMENTS_FR; // libra evaluations - - // PCS commitments - proofLength += (logN - 1) * NUM_ELEMENTS_COMM; // Gemini Fold commitments - proofLength += NUM_ELEMENTS_COMM * 2; // Shplonk Q and KZG W commitments - - // Pairing points - proofLength += PAIRING_POINTS_SIZE; // pairing inputs carried on public inputs - - return proofLength * 32; - } - - function loadVerificationKey() internal pure virtual returns (Honk.VerificationKey memory); -} - -contract XHonkVerifier is BaseZKHonkVerifier(N, LOG_N, VK_HASH, NUMBER_OF_PUBLIC_INPUTS) { - function loadVerificationKey() internal pure override returns (Honk.VerificationKey memory) { - return HonkVerificationKey.loadVerificationKey(); - } -} diff --git a/solidity/archived/login/zk/XZkVerifier.sol b/solidity/archived/login/zk/XZkVerifier.sol deleted file mode 100644 index a5086d3a..00000000 --- a/solidity/archived/login/zk/XZkVerifier.sol +++ /dev/null @@ -1,561 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {IZkSessionVerifier} from "./IZkSessionVerifier.sol"; -import {INotary} from "../../notary/INotary.sol"; - -interface IHonkVerifier { - function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool); -} - -/// X ZK identity verifier — small token-only proof + two TLSN attestations. -/// Public-input layout is documented inline above the offset constants. -/// Registry (not this contract) enforces walletAddress semantics. -contract XZkVerifier is IZkSessionVerifier, Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { - struct TokenAttestation { - bytes32 bearerHash; - uint32 bearerRangeStart; - uint32 bearerRangeEnd; - bytes sentRevealed; - uint64 timestamp; - bytes notarySignature; - } - - /// /me sentRevealed = concat of two TLSN-revealed ranges: - /// [0, bearerRangeStart) — request line + headers up to and - /// including `authorization: Bearer `. - /// Ends at `sentPrefixEnd == bearerRangeStart`. - /// [bearerRangeEnd, bearerRangeEnd + 2) — exactly the 2 bytes after - /// the bearer, which MUST be CRLF. - /// Ends at `sentSuffixEnd == bearerRangeEnd + 2`. - /// Anchoring the end of the bearer range canonicalizes `bearer_len`, so - /// one real OAuth bearer hashes to exactly one nullifier (H1). - struct MeAttestation { - bytes32 bearerHash; - uint32 bearerRangeStart; - uint32 bearerRangeEnd; - bytes sentRevealed; - uint32 sentPrefixEnd; - uint32 sentSuffixEnd; - bytes recvRevealed; - string handle; - /// Immutable platform user-id from the same /me response (X `"id":"…"`). - /// "" when the prover did not reveal it (handle-key fallback). - string userId; - address sessionAddr; - uint64 timestamp; - bytes notarySignature; - } - - struct XProof { - bytes proof; - bytes32[] publicInputs; - TokenAttestation tokenAttest; - MeAttestation meAttest; - } - - // PI layout: one byte per slot, low-byte of each bytes32. - // [ 0..32) bearer_hash_token SHA256(bearer || blinder_token) - // [ 32..64) bearer_hash_me SHA256(bearer || blinder_me) - // [ 64..96) nullifier keccak256(bearer_padded || len.be32) - // [ 96..116) wallet_address - // [116..136) session_addr - uint256 private constant OFF_BEARER_HASH_TOKEN = 0; - uint256 private constant OFF_BEARER_HASH_ME = 32; - uint256 private constant OFF_NULLIFIER = 64; - uint256 private constant OFF_WALLET_ADDR = 96; - uint256 private constant OFF_SESSION_ADDR = 116; - uint256 private constant TOTAL_PUBLIC_INPUTS = 136; - - uint256 public constant CLOCK_SKEW_GRACE = 5 minutes; - uint256 private constant ZK_PROOF_TTL = 10 minutes; - uint256 private constant MAX_CLIENT_ID_LEN = 64; - /// Allowlist size cap. Verification scans the revealed request once per - /// entry, so this bounds worst-case gas for `_anyClientIdMatches`. - uint256 private constant MAX_CLIENT_IDS = 16; - uint256 private constant MAX_REVEALED_LEN = 4096; - uint256 private constant MAX_HANDLE_LEN = 32; - - /// Domain-sep tags for the single notary key. - bytes32 internal constant OP_TOKEN_ATTEST = keccak256("XZkVerifier.token.v1"); - bytes32 internal constant OP_ME_ATTEST = keccak256("XZkVerifier.me.v1"); - - /// The Notary contract whose attestations this accepts. The digests still - /// bind `address(this)` + the OP tags; only the check itself is shared. - INotary public notaryContract; - IHonkVerifier public honkVerifier; - string public endpoint; - string public handlePrefix; - string public override platformName; - - /// Allowed OAuth `client_id`s. A /token proof passes when the revealed - /// request carries ANY of them (see `_anyClientIdMatches`). Kept as an array - /// because matching is a substring search over the revealed request, not a - /// lookup by a caller-supplied id. - bytes[] private _clientIds; - /// keccak256(client_id) => allowed. Mirrors `_clientIds` for O(1) membership - /// and dedupe. - mapping(bytes32 => bool) public isClientIdAllowed; - /// When true, ANY client_id is accepted. - /// - /// Deliberately a separate flag: an EMPTY allowlist means DENY ALL, never - /// "allow everyone". Conflating "not configured yet" with "intentionally - /// permissive" would let an unseeded deploy, a botched migration, or removal - /// of the last entry silently drop the app binding (fail-open). Enabling this - /// removes the proof that the notarized /token came from a known OAuth app — - /// any app's token can then register a session. That is the permissionless - /// model, so it must be an explicit, evented choice. - bool public openClientIds; - - /// X `/me` numeric id is JSON `"id":""`. Fixed for this platform, - /// so it's a constant rather than an init param (no deploy-signature change). - string private constant ID_PREFIX = "\"id\":\""; - string private constant ID_SUFFIX = "\""; - - error InvalidPublicInputsLength(); - error ZkVerificationFailed(); - error NotaryVerificationFailed(); - error FutureProof(); - error StaleProof(); - error BearerHashMismatch(); - error ClientIdMismatch(); - error MeRequestPrefixMismatch(); - error MeBearerAdjacencyMismatch(); - error MeBearerEndAdjacencyMismatch(); - error MeBearerEndNotCrlf(); - error MeBadSentLayout(); - error HandleNotFound(); - error IdNotFound(); - error EmptyHandle(); - error HandleTooLong(); - error RevealedTooLong(); - error SessionAddrMismatch(); - error ClientIdAlreadyAllowed(); - error ClientIdNotAllowed(); - error TooManyClientIds(); - error BadClientIdLength(); - - event ClientIdAdded(bytes clientId); - event ClientIdRemoved(bytes clientId); - /// Emitted on every permissive-mode toggle so the trust change is auditable. - event OpenClientIdsSet(bool open); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @param _notaryContract The shared Notary contract (INotary). Notary key - /// rotation happens THERE; this contract holds only the pointer. - function initialize( - address _owner, - address _notaryContract, - IHonkVerifier _honkVerifier, - bytes calldata _xClientId, - string calldata _endpoint, - string calldata _handlePrefix, - string calldata _platformName - ) external initializer { - __Ownable_init(_owner); - __UUPSUpgradeable_init(); - require(_notaryContract != address(0), "zero notary contract"); - require(address(_honkVerifier) != address(0), "zero verifier"); - require(bytes(_endpoint).length > 0, "empty endpoint"); - require(bytes(_handlePrefix).length > 0, "empty prefix"); - require(bytes(_platformName).length > 0, "empty platform"); - notaryContract = INotary(_notaryContract); - honkVerifier = _honkVerifier; - // Seed the allowlist with the deploying app so existing deployments and - // the unchanged deploy signature keep working. - _addClientId(_xClientId); - endpoint = _endpoint; - handlePrefix = _handlePrefix; - platformName = _platformName; - } - - /// The current notary signer, read through the Notary contract. - function notary() external view returns (address) { - return notaryContract.notary(); - } - - function setHonkVerifier(IHonkVerifier _v) external onlyOwner { - require(address(_v) != address(0), "zero verifier"); - honkVerifier = _v; - } - - /// Allow one more OAuth app's `client_id` to register sessions here. - function addClientId(bytes calldata _id) external onlyOwner { - _addClientId(_id); - } - - /// Revoke a `client_id`. Removing the last entry leaves the allowlist EMPTY, - /// which denies everything (unless `openClientIds` is explicitly on) — that - /// fail-closed outcome is intentional. - function removeClientId(bytes calldata _id) external onlyOwner { - bytes32 key = keccak256(_id); - if (!isClientIdAllowed[key]) revert ClientIdNotAllowed(); - delete isClientIdAllowed[key]; - - uint256 n = _clientIds.length; - for (uint256 i = 0; i < n; i++) { - if (keccak256(_clientIds[i]) == key) { - _clientIds[i] = _clientIds[n - 1]; - _clientIds.pop(); - break; - } - } - emit ClientIdRemoved(_id); - } - - /// Accept ANY client_id. See `openClientIds` — this drops the app binding - /// and is a trust-model change, not a config knob. - function setOpenClientIds(bool _open) external onlyOwner { - openClientIds = _open; - emit OpenClientIdsSet(_open); - } - - function clientIdCount() external view returns (uint256) { - return _clientIds.length; - } - - function clientIdAt(uint256 _i) external view returns (bytes memory) { - return _clientIds[_i]; - } - - function _addClientId(bytes memory _id) internal { - if (_id.length == 0 || _id.length > MAX_CLIENT_ID_LEN) revert BadClientIdLength(); - if (_clientIds.length >= MAX_CLIENT_IDS) revert TooManyClientIds(); - bytes32 key = keccak256(_id); - if (isClientIdAllowed[key]) revert ClientIdAlreadyAllowed(); - isClientIdAllowed[key] = true; - _clientIds.push(_id); - emit ClientIdAdded(_id); - } - - function setEndpoint(string calldata _endpoint) external onlyOwner { - require(bytes(_endpoint).length > 0, "empty endpoint"); - endpoint = _endpoint; - } - - function setHandlePrefix(string calldata _prefix) external onlyOwner { - require(bytes(_prefix).length > 0, "empty prefix"); - handlePrefix = _prefix; - } - - function setPlatformName(string calldata _name) external onlyOwner { - require(bytes(_name).length > 0, "empty platform"); - platformName = _name; - } - - function verifyAndExtract(bytes calldata payload) - external - view - override - returns ( - string memory handle, - address sessionKey, - address walletAddress, - uint256 expiresAt, - bytes32 nullifier, - string memory userId - ) - { - XProof calldata p = _decode(payload); - - if (p.publicInputs.length != TOTAL_PUBLIC_INPUTS) revert InvalidPublicInputsLength(); - if (p.tokenAttest.sentRevealed.length > MAX_REVEALED_LEN) revert RevealedTooLong(); - if (p.meAttest.sentRevealed.length > MAX_REVEALED_LEN) revert RevealedTooLong(); - if (p.meAttest.recvRevealed.length > MAX_REVEALED_LEN) revert RevealedTooLong(); - if (bytes(p.meAttest.handle).length == 0) revert EmptyHandle(); - if (bytes(p.meAttest.handle).length > MAX_HANDLE_LEN) revert HandleTooLong(); - - _checkTimestamp(p.tokenAttest.timestamp); - _checkTimestamp(p.meAttest.timestamp); - - // Dual-blinder cross-bind: same bearer hashes to BOTH commits via the - // circuit, but with different blinders (one per TLSN session). - if (_extractBytes32(p.publicInputs, OFF_BEARER_HASH_TOKEN) != p.tokenAttest.bearerHash) { - revert BearerHashMismatch(); - } - if (_extractBytes32(p.publicInputs, OFF_BEARER_HASH_ME) != p.meAttest.bearerHash) { - revert BearerHashMismatch(); - } - - // sessionAddr from PI must equal notary-signed value in meAttest. - address piSession = _extractAddress(p.publicInputs, OFF_SESSION_ADDR); - if (piSession != p.meAttest.sessionAddr) revert SessionAddrMismatch(); - - // /token: client_id bound + notary sig. - if (!_anyClientIdMatches(p.tokenAttest.sentRevealed)) revert ClientIdMismatch(); - _verifyTokenSig(p.tokenAttest); - - // /me: request structure + handle present in RECV + notary sig. - _verifyMeRequestStructure(p.meAttest); - _verifyHandleInRecv(p.meAttest); - _verifyIdInRecv(p.meAttest); - _verifyMeSig(p.meAttest); - - // ZK proof (token-only). - if (!honkVerifier.verify(p.proof, p.publicInputs)) revert ZkVerificationFailed(); - - handle = p.meAttest.handle; - userId = p.meAttest.userId; - sessionKey = piSession; - walletAddress = _extractAddress(p.publicInputs, OFF_WALLET_ADDR); - bytes32 rawNullifier = _extractBytes32(p.publicInputs, OFF_NULLIFIER); - nullifier = keccak256(abi.encode(keccak256(bytes(platformName)), rawNullifier)); - // TTL anchored to the OLDER of the two attestations -- the proof is - // only as fresh as its stalest input. - uint256 anchorTs = uint256(p.tokenAttest.timestamp) < uint256(p.meAttest.timestamp) - ? uint256(p.tokenAttest.timestamp) - : uint256(p.meAttest.timestamp); - expiresAt = anchorTs + ZK_PROOF_TTL; - } - - function _extractBytes32(bytes32[] calldata pi, uint256 offset) internal pure returns (bytes32 result) { - assembly ("memory-safe") { - let r := 0 - let src := add(pi.offset, mul(offset, 0x20)) - for { let i := 0 } lt(i, 32) { i := add(i, 1) } { - r := or(r, shl(mul(sub(31, i), 8), and(calldataload(add(src, mul(i, 0x20))), 0xff))) - } - result := r - } - } - - function _extractAddress(bytes32[] calldata pi, uint256 offset) internal pure returns (address result) { - assembly ("memory-safe") { - let r := 0 - let src := add(pi.offset, mul(offset, 0x20)) - for { let i := 0 } lt(i, 20) { i := add(i, 1) } { - r := or(r, shl(mul(sub(19, i), 8), and(calldataload(add(src, mul(i, 0x20))), 0xff))) - } - result := r - } - } - - function _decode(bytes calldata payload) internal pure returns (XProof calldata p) { - assembly ("memory-safe") { - p := add(payload.offset, calldataload(payload.offset)) - } - } - - function _checkTimestamp(uint64 ts) internal view { - if (ts > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); - if (block.timestamp > uint256(ts) + ZK_PROOF_TTL) revert StaleProof(); - } - - function _verifyMeRequestStructure(MeAttestation calldata att) internal view { - bytes memory prefix = abi.encodePacked("GET ", endpoint, " HTTP/1.1\r\n"); - bytes memory authSuffix = bytes("authorization: Bearer "); - - bytes memory sent = att.sentRevealed; - - // H1: sentRevealed is concat of [prefix part] ++ [2-byte CRLF post-bearer]. - // Length must be exactly `prefixLen + 2`. `prefixLen` = sentPrefixEnd - // because the prefix range starts at offset 0. - uint256 prefixLen = uint256(att.sentPrefixEnd); - if (sent.length != prefixLen + 2) revert MeBadSentLayout(); - if (prefixLen < prefix.length + authSuffix.length) revert MeRequestPrefixMismatch(); - - // Prefix part [0..prefixLen) must start with `GET ... HTTP/1.1\r\n` and - // end with `authorization: Bearer `. - for (uint256 i = 0; i < prefix.length; i++) { - if (sent[i] != prefix[i]) revert MeRequestPrefixMismatch(); - } - uint256 suffixStart = prefixLen - authSuffix.length; - for (uint256 i = 0; i < authSuffix.length; i++) { - if (sent[suffixStart + i] != authSuffix[i]) revert MeRequestPrefixMismatch(); - } - - // Start adjacency: bearer starts immediately after the revealed prefix. - if (att.sentPrefixEnd != att.bearerRangeStart) revert MeBearerAdjacencyMismatch(); - - // End adjacency (H1): the second revealed range covers exactly the 2 - // bytes immediately after the bearer. - if (att.sentSuffixEnd != att.bearerRangeEnd + 2) revert MeBearerEndAdjacencyMismatch(); - - // The trailing 2 bytes of sentRevealed are the post-bearer bytes. They - // MUST be CRLF. Bearer is base64url; `\r\n` cannot appear inside it, - // so truncating the bearer range and re-anchoring on a fake CRLF - // elsewhere in the request is impossible without rewriting the wire. - if (sent[prefixLen] != 0x0d) revert MeBearerEndNotCrlf(); - if (sent[prefixLen + 1] != 0x0a) revert MeBearerEndNotCrlf(); - } - - function _verifyHandleInRecv(MeAttestation calldata att) internal view { - bytes memory needle = abi.encodePacked(handlePrefix, att.handle, '"'); - bytes memory prefix = bytes(handlePrefix); - bytes memory haystack = att.recvRevealed; - if (prefix.length == 0 || haystack.length < needle.length) revert HandleNotFound(); - - // Single pass: count `handlePrefix` occurrences and verify the full - // needle matches at the unique prefix hit. Rejecting >1 prefix hit - // defends against an unescaped upstream field containing a synthetic - // `"username":""`. - uint256 needleStart = type(uint256).max; - uint256 hits = 0; - uint256 limit = haystack.length - prefix.length + 1; - for (uint256 i = 0; i < limit; i++) { - bool m = true; - for (uint256 j = 0; j < prefix.length; j++) { - if (haystack[i + j] != prefix[j]) { - m = false; - break; - } - } - if (m) { - hits++; - if (hits > 1) revert HandleNotFound(); - needleStart = i; - } - } - if (hits == 0) revert HandleNotFound(); - if (needleStart + needle.length > haystack.length) revert HandleNotFound(); - for (uint256 j = 0; j < needle.length; j++) { - if (haystack[needleStart + j] != needle[j]) revert HandleNotFound(); - } - } - - /// Verify the immutable user-id is present in the notary-attested /me recv - /// bytes as `"id":""`. Empty userId → skip (handle-key fallback). - /// Same unique-prefix-hit guard as the handle check. - function _verifyIdInRecv(MeAttestation calldata att) internal pure { - // Id is required — identity binds on the immutable platform id, not the - // mutable handle. An attestation without a revealed id is rejected. - if (bytes(att.userId).length == 0) revert IdNotFound(); - bytes memory needle = abi.encodePacked(ID_PREFIX, att.userId, ID_SUFFIX); - bytes memory prefix = bytes(ID_PREFIX); - bytes memory haystack = att.recvRevealed; - if (haystack.length < needle.length) revert IdNotFound(); - - uint256 needleStart = type(uint256).max; - uint256 hits = 0; - uint256 limit = haystack.length - prefix.length + 1; - for (uint256 i = 0; i < limit; i++) { - bool m = true; - for (uint256 j = 0; j < prefix.length; j++) { - if (haystack[i + j] != prefix[j]) { - m = false; - break; - } - } - if (m) { - hits++; - if (hits > 1) revert IdNotFound(); - needleStart = i; - } - } - if (hits == 0) revert IdNotFound(); - if (needleStart + needle.length > haystack.length) revert IdNotFound(); - for (uint256 j = 0; j < needle.length; j++) { - if (haystack[needleStart + j] != needle[j]) revert IdNotFound(); - } - } - - function _verifyTokenSig(TokenAttestation calldata att) internal view { - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(this), - keccak256(bytes(platformName)), - OP_TOKEN_ATTEST, - att.bearerHash, - uint256(att.bearerRangeStart), - uint256(att.bearerRangeEnd), - keccak256(att.sentRevealed), - uint256(att.timestamp) - ) - ); - if (!notaryContract.verify(digest, att.notarySignature)) revert NotaryVerificationFailed(); - } - - function _verifyMeSig(MeAttestation calldata att) internal view { - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(this), - keccak256(bytes(platformName)), - OP_ME_ATTEST, - att.bearerHash, - uint256(att.bearerRangeStart), - uint256(att.bearerRangeEnd), - keccak256(att.sentRevealed), - uint256(att.sentPrefixEnd), - uint256(att.sentSuffixEnd), - keccak256(att.recvRevealed), - keccak256(bytes(att.handle)), - keccak256(bytes(att.userId)), - att.sessionAddr, - uint256(att.timestamp) - ) - ); - if (!notaryContract.verify(digest, att.notarySignature)) revert NotaryVerificationFailed(); - } - - /// True when the revealed /token request carries an accepted `client_id`. - /// An empty allowlist matches nothing: "unconfigured" denies rather than - /// allows. - /// - /// Scans the revealed request ONCE, extracts the `client_id=` value, and - /// answers from the `isClientIdAllowed` mirror. The previous shape re-ran - /// the full O(revealedLen) scan per allowlist entry, so a proof from the - /// last-listed app paid MAX_CLIENT_IDS times the dominant cost of - /// `verifyAndExtract` — enough to push `register_session_zk` toward the - /// block gas limit for exactly the multi-app case the allowlist exists to - /// serve. Cost is now independent of how many ids are configured. - function _anyClientIdMatches(bytes memory revealed) internal view returns (bool) { - if (openClientIds) return true; - if (_clientIds.length == 0) return false; - - bytes10 needle = "client_id="; - uint256 revealedLen = revealed.length; - if (revealedLen < 11) return false; - - uint256 limit = revealedLen - 10; - for (uint256 i = 0; i < limit; i++) { - bool m = true; - for (uint256 j = 0; j < 10; j++) { - if (revealed[i + j] != needle[j]) { - m = false; - break; - } - } - if (!m) continue; - - // Value runs to `&`, CR, or end-of-buffer. Bounded by - // MAX_CLIENT_ID_LEN so a pathological revealed buffer cannot make - // this scan unbounded; an over-long value simply fails to match. - uint256 start = i + 10; - uint256 end = start; - while (end < revealedLen) { - bytes1 b = revealed[end]; - if (b == 0x26 || b == 0x0d) break; - unchecked { - ++end; - } - if (end - start > MAX_CLIENT_ID_LEN) break; - } - uint256 valueLen = end - start; - if (valueLen == 0 || valueLen > MAX_CLIENT_ID_LEN) continue; - - bytes memory value = new bytes(valueLen); - for (uint256 j = 0; j < valueLen; j++) { - value[j] = revealed[start + j]; - } - if (isClientIdAllowed[keccak256(value)]) return true; - } - return false; - } - - function _authorizeUpgrade(address) internal override onlyOwner {} - - /// @dev Renouncing ownership is disabled: a zero owner would permanently - /// brick the UUPS upgrade path and every admin function. - function renounceOwnership() public pure override { - revert("renounce disabled"); - } -} diff --git a/solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol b/solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol deleted file mode 100644 index 3839aa3d..00000000 --- a/solidity/archived/login/zk/test/XZkVerifierAllowlist.t.sol +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {XZkVerifier} from "../XZkVerifier.sol"; -import {XZkVerifierBindingsTest} from "./XZkVerifierBindings.t.sol"; - -/// Multi-app `client_id` allowlist on XZkVerifier. -/// -/// The security property under test is FAIL-CLOSED: an empty allowlist denies -/// every client_id. "Not configured yet" must never be read as "allow everyone", -/// otherwise an unseeded deploy or the removal of the last entry would silently -/// drop the app binding. Permissive mode exists, but only as the explicit -/// `openClientIds` flag. -/// -/// Inherits the bindings test for its fixtures and attestation builders. -contract XZkVerifierAllowlistTest is XZkVerifierBindingsTest { - address constant WALLET = address(0x1111); - address constant SESSION = address(0x5E5510); - bytes32 constant BEARER_HASH = bytes32(uint256(0xBEEF)); - bytes32 constant NULLIFIER = bytes32(uint256(0xC0FFEE)); - - bytes constant OTHER_CLIENT_ID = bytes("second-app-id-456"); - - /// Payload whose /token body carries `client_id=`, notary-signed - /// over that exact body. Mirrors _buildTokenAttest but with a caller-chosen - /// client_id so a non-allowlisted app can be exercised. - function _payloadWithClientId(bytes memory clientId) internal view returns (bytes memory) { - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(BEARER_HASH, uint64(block.timestamp)); - tokenAttest.sentRevealed = abi.encodePacked("grant_type=authorization_code&client_id=", clientId, "&code=xyz"); - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.token.v1"), - tokenAttest.bearerHash, - uint256(tokenAttest.bearerRangeStart), - uint256(tokenAttest.bearerRangeEnd), - keccak256(tokenAttest.sentRevealed), - uint256(tokenAttest.timestamp) - ) - ); - tokenAttest.notarySignature = _sign(NOTARY_PK, digest); - - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(BEARER_HASH, "alice", SESSION, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(BEARER_HASH, BEARER_HASH, NULLIFIER, WALLET, SESSION), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - return abi.encode(p); - } - - function _verify(bytes memory payload) internal { - vm.prank(WALLET); - verifier.verifyAndExtract(payload); - } - - // ─── Seeding ───────────────────────────────────────────────────── - - /// initialize() seeds the deploying app, so an unchanged deploy keeps working. - function test_initialize_seedsDeployingClientId() public view { - assertEq(verifier.clientIdCount(), 1); - assertTrue(verifier.isClientIdAllowed(keccak256(X_CLIENT_ID))); - assertEq(verifier.clientIdAt(0), X_CLIENT_ID); - assertFalse(verifier.openClientIds(), "must not start permissive"); - } - - // ─── Fail-closed ───────────────────────────────────────────────── - - /// THE fail-closed property: with the allowlist emptied, every client_id is - /// rejected — including the one that worked a moment ago. A regression that - /// treated "empty" as "allow all" would make this pass verification instead. - function test_emptyAllowlist_deniesEveryClientId() public { - vm.prank(OWNER); - verifier.removeClientId(X_CLIENT_ID); - assertEq(verifier.clientIdCount(), 0); - - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - _verify(_payloadWithClientId(X_CLIENT_ID)); - - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - } - - function test_unlistedClientId_rejected() public { - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - } - - // ─── Multi-app ─────────────────────────────────────────────────── - - function test_addClientId_admitsSecondApp_bothWork() public { - vm.prank(OWNER); - verifier.addClientId(OTHER_CLIENT_ID); - - assertEq(verifier.clientIdCount(), 2); - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - _verify(_payloadWithClientId(X_CLIENT_ID)); - } - - function test_removeClientId_revokesOnlyThatApp() public { - vm.startPrank(OWNER); - verifier.addClientId(OTHER_CLIENT_ID); - verifier.removeClientId(X_CLIENT_ID); - vm.stopPrank(); - - assertEq(verifier.clientIdCount(), 1); - assertFalse(verifier.isClientIdAllowed(keccak256(X_CLIENT_ID))); - - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - _verify(_payloadWithClientId(X_CLIENT_ID)); - } - - // ─── Permissive mode ───────────────────────────────────────────── - - function test_openClientIds_acceptsUnknownApp() public { - vm.prank(OWNER); - verifier.setOpenClientIds(true); - - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - } - - /// Permissive mode overrides even an empty allowlist — it is the flag, not - /// emptiness, that grants access. - function test_openClientIds_worksWithEmptyAllowlist() public { - vm.startPrank(OWNER); - verifier.removeClientId(X_CLIENT_ID); - verifier.setOpenClientIds(true); - vm.stopPrank(); - - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - } - - function test_openClientIds_offAgain_deniesUnknownApp() public { - vm.startPrank(OWNER); - verifier.setOpenClientIds(true); - verifier.setOpenClientIds(false); - vm.stopPrank(); - - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - _verify(_payloadWithClientId(OTHER_CLIENT_ID)); - } - - function test_setOpenClientIds_emitsEvent() public { - vm.expectEmit(false, false, false, true, address(verifier)); - emit XZkVerifier.OpenClientIdsSet(true); - vm.prank(OWNER); - verifier.setOpenClientIds(true); - } - - // ─── Admin guards ──────────────────────────────────────────────── - - function test_addClientId_duplicate_reverts() public { - vm.prank(OWNER); - vm.expectRevert(XZkVerifier.ClientIdAlreadyAllowed.selector); - verifier.addClientId(X_CLIENT_ID); - } - - function test_removeClientId_unknown_reverts() public { - vm.prank(OWNER); - vm.expectRevert(XZkVerifier.ClientIdNotAllowed.selector); - verifier.removeClientId(OTHER_CLIENT_ID); - } - - function test_addClientId_emptyOrTooLong_reverts() public { - vm.startPrank(OWNER); - vm.expectRevert(XZkVerifier.BadClientIdLength.selector); - verifier.addClientId(bytes("")); - - vm.expectRevert(XZkVerifier.BadClientIdLength.selector); - verifier.addClientId(new bytes(65)); // MAX_CLIENT_ID_LEN is 64 - vm.stopPrank(); - } - - /// Verification scans once per entry, so the list is capped to bound gas. - function test_addClientId_capEnforced() public { - vm.startPrank(OWNER); - // One seeded at init; fill to the cap of 16. - for (uint256 i = 1; i < 16; i++) { - verifier.addClientId(abi.encodePacked("app-", i)); - } - assertEq(verifier.clientIdCount(), 16); - - vm.expectRevert(XZkVerifier.TooManyClientIds.selector); - verifier.addClientId(bytes("one-too-many")); - vm.stopPrank(); - } - - function test_adminFunctions_onlyOwner() public { - address stranger = address(0xBAD); - - vm.startPrank(stranger); - vm.expectRevert(); - verifier.addClientId(OTHER_CLIENT_ID); - - vm.expectRevert(); - verifier.removeClientId(X_CLIENT_ID); - - vm.expectRevert(); - verifier.setOpenClientIds(true); - vm.stopPrank(); - } -} diff --git a/solidity/archived/login/zk/test/XZkVerifierBindings.t.sol b/solidity/archived/login/zk/test/XZkVerifierBindings.t.sol deleted file mode 100644 index b88704b8..00000000 --- a/solidity/archived/login/zk/test/XZkVerifierBindings.t.sol +++ /dev/null @@ -1,658 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../Registry.sol"; -import {WalletFactory} from "../../WalletFactory.sol"; -import {WebWallet} from "../../WebWallet.sol"; -import {XZkVerifier, IHonkVerifier} from "../XZkVerifier.sol"; -import {IZkSessionVerifier} from "../IZkSessionVerifier.sol"; -import {Notary} from "../../../notary/Notary.sol"; -import {deployNotary} from "../../../notary/test/DeployNotary.sol"; - -/// Mock honk verifier — always returns the configured value. Lets us -/// exercise XZkVerifier wiring without a real Honk proof. -contract MockHonkVerifier is IHonkVerifier { - bool public ret = true; - - function setRet(bool v) external { - ret = v; - } - - function verify(bytes calldata, bytes32[] calldata) external view returns (bool) { - return ret; - } -} - -/// Bindings for the new token-only XZkVerifier. The on-chain check is -/// orthogonal to the actual Honk proof contents — we cover: -/// 1. Happy path: notary-signed token+me attestations + matching PI. -/// 2. PI walletAddress is returned unchanged (Registry, not verifier, enforces binding). -/// 3. Cross-bind revert: differing bearerHash. -/// 4. sessionAddr mismatch revert: PI sessionAddr != meAttest.sessionAddr. -/// 5. /me request-line structure revert. -/// 6. handle not found in recvRevealed revert. -/// 7. client_id mismatch revert. -contract XZkVerifierBindingsTest is Test { - Registry registry; - WalletFactory factory; - XZkVerifier verifier; - MockHonkVerifier honk; - - uint256 constant NOTARY_PK = 0xA001; - address NOTARY; - Notary notaryContract; - address constant OWNER = address(0xA11CE); - string constant PLATFORM = "api.x.com"; - string constant ENDPOINT = "/2/users/me"; - string constant HANDLE_PREFIX = "\"username\":\""; - bytes constant X_CLIENT_ID = bytes("dev-client-id-123"); - - function setUp() public { - NOTARY = vm.addr(NOTARY_PK); - notaryContract = deployNotary(OWNER, NOTARY); - - WebWallet walletImpl = new WebWallet(); - WalletFactory fImpl = new WalletFactory(); - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (OWNER, address(walletImpl), address(1))) - ) - ) - ); - - Registry rImpl = new Registry(); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall( - Registry.initialize, (address(notaryContract), address(0xBE), address(factory), OWNER) - ) - ) - ) - ); - - vm.prank(OWNER); - factory.setRegistry(address(registry)); - - honk = new MockHonkVerifier(); - XZkVerifier xImpl = new XZkVerifier(); - verifier = XZkVerifier( - address( - new ERC1967Proxy( - address(xImpl), - abi.encodeCall( - XZkVerifier.initialize, - ( - OWNER, - address(notaryContract), - IHonkVerifier(address(honk)), - X_CLIENT_ID, - ENDPOINT, - HANDLE_PREFIX, - PLATFORM - ) - ) - ) - ) - ); - - vm.prank(OWNER); - registry.setZkVerifier(PLATFORM, IZkSessionVerifier(address(verifier))); - vm.warp(1_000_000); - } - - // ─── Builders ──────────────────────────────────────────────────── - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _tokenSentRevealed() internal pure returns (bytes memory) { - // `client_id=` somewhere in the body satisfies the matcher. - return abi.encodePacked("grant_type=authorization_code&client_id=", X_CLIENT_ID, "&code=xyz"); - } - - /// Returns the prefix part of /me sentRevealed (ending with - /// `authorization: Bearer `). The full sentRevealed under H1 is this - /// prefix concatenated with the 2-byte `\r\n` CRLF trailer. - function _meSentRevealedPrefix() internal view returns (bytes memory) { - return abi.encodePacked("GET ", ENDPOINT, " HTTP/1.1\r\nhost: api.x.com\r\nauthorization: Bearer "); - } - - function _meRecvRevealed(string memory handle) internal pure returns (bytes memory) { - // Minimal JSON snippet with the handle quoted. - return abi.encodePacked("{\"data\":{\"id\":\"42\",", HANDLE_PREFIX, handle, "\",\"name\":\"X\"}}"); - } - - function _buildTokenAttest(bytes32 bearerHash, uint64 ts) - internal - view - returns (XZkVerifier.TokenAttestation memory att) - { - att.bearerHash = bearerHash; - att.bearerRangeStart = 100; - att.bearerRangeEnd = 200; - att.sentRevealed = _tokenSentRevealed(); - att.timestamp = ts; - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.token.v1"), - att.bearerHash, - uint256(att.bearerRangeStart), - uint256(att.bearerRangeEnd), - keccak256(att.sentRevealed), - uint256(att.timestamp) - ) - ); - att.notarySignature = _sign(NOTARY_PK, digest); - } - - function _buildMeAttest(bytes32 bearerHash, string memory handle, address sessionAddr, uint64 ts) - internal - view - returns (XZkVerifier.MeAttestation memory att) - { - bytes memory prefix = _meSentRevealedPrefix(); - bytes memory recv = _meRecvRevealed(handle); - att.bearerHash = bearerHash; - att.bearerRangeStart = uint32(prefix.length); - att.bearerRangeEnd = att.bearerRangeStart + 100; - // sentRevealed = prefix bytes + CRLF (2 bytes) — H1 layout. - att.sentRevealed = abi.encodePacked(prefix, bytes("\r\n")); - att.sentPrefixEnd = uint32(prefix.length); - att.sentSuffixEnd = att.bearerRangeEnd + 2; - att.recvRevealed = recv; - att.handle = handle; - att.userId = "42"; // matches the "id":"42" snippet in _meRecvRevealed - att.sessionAddr = sessionAddr; - att.timestamp = ts; - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.me.v1"), - att.bearerHash, - uint256(att.bearerRangeStart), - uint256(att.bearerRangeEnd), - keccak256(att.sentRevealed), - uint256(att.sentPrefixEnd), - uint256(att.sentSuffixEnd), - keccak256(att.recvRevealed), - keccak256(bytes(att.handle)), - keccak256(bytes(att.userId)), - att.sessionAddr, - uint256(att.timestamp) - ) - ); - att.notarySignature = _sign(NOTARY_PK, digest); - } - - /// Layout: bearer_hash_token[32] | bearer_hash_me[32] | nullifier[32] - /// | wallet[20] | session[20] = 136 slots, one byte per slot. - function _buildPI( - bytes32 bearerHashToken, - bytes32 bearerHashMe, - bytes32 nullifier, - address wallet, - address sessionAddr - ) internal pure returns (bytes32[] memory pi) { - pi = new bytes32[](136); - for (uint256 i = 0; i < 32; i++) { - pi[i] = bytes32(uint256(uint8(bearerHashToken[i]))); - pi[32 + i] = bytes32(uint256(uint8(bearerHashMe[i]))); - pi[64 + i] = bytes32(uint256(uint8(nullifier[i]))); - } - bytes20 w = bytes20(wallet); - bytes20 s = bytes20(sessionAddr); - for (uint256 i = 0; i < 20; i++) { - pi[96 + i] = bytes32(uint256(uint8(w[i]))); - pi[116 + i] = bytes32(uint256(uint8(s[i]))); - } - } - - function _buildPayload( - bytes32 bearerHash, - bytes32 nullifier, - address wallet, - address sessionAddr, - string memory handle - ) internal view returns (bytes memory) { - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, handle, sessionAddr, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, wallet, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - return abi.encode(p); - } - - // ─── Tests ─────────────────────────────────────────────────────── - - function test_happyPath() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address wallet = address(0x1111); - - bytes memory payload = _buildPayload(bearerHash, nullifier, wallet, sessionAddr, "alice"); - - vm.prank(wallet); - ( - string memory handle, - address sk, - address piWallet, - uint256 expiresAt, - bytes32 returnedNullifier, - string memory rUserId - ) = verifier.verifyAndExtract(payload); - - assertEq(handle, "alice"); - assertEq(sk, sessionAddr); - assertEq(piWallet, wallet); - assertGt(expiresAt, block.timestamp); - bytes32 expected = keccak256(abi.encode(keccak256(bytes(PLATFORM)), nullifier)); - assertEq(returnedNullifier, expected); - } - - function test_walletAddress_returnedFromPi() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - - // Verify across three distinct wallet PI values to catch a regression - // where the verifier silently masks/truncates/replaces the returned - // walletAddress. Registry consumes this exact value to enforce the - // register-zero / link-msg.sender contract. - address[3] memory wallets = [ - address(0), - address(0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF), - address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) // Anvil acct 0 - ]; - for (uint256 i = 0; i < wallets.length; i++) { - bytes memory payload = _buildPayload(bearerHash, nullifier, wallets[i], sessionAddr, "alice"); - (,, address piWallet,,,) = verifier.verifyAndExtract(payload); - assertEq(piWallet, wallets[i]); - } - } - - function test_bearerHashMismatch_token_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = - _buildTokenAttest(bytes32(uint256(0xBADBEEF)), uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.BearerHashMismatch.selector); - verifier.verifyAndExtract(payload); - } - - /// Symmetric: PI[bearer_hash_me] != meAttest.bearerHash must revert too. - /// Catches the case where /token attestation matches but /me doesn't -- - /// the dual-blinder cross-bind enforces BOTH commits independently. - function test_bearerHashMismatch_me_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bytes32(uint256(0xBADBEEF)), "alice", sessionAddr, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.BearerHashMismatch.selector); - verifier.verifyAndExtract(payload); - } - - /// PI[bearer_hash_token] and PI[bearer_hash_me] must each match THEIR - /// own attestation; they can legitimately differ (two different - /// blinders) but each must equal the corresponding notary commit. - /// Tampering PI[token] alone while leaving attestations valid still - /// reverts on the cross-bind check. - function test_bearerHashMismatch_piTokenTampered_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bytes32(uint256(0xCAFEBABE)), bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.BearerHashMismatch.selector); - verifier.verifyAndExtract(payload); - } - - function test_sessionAddrMismatch_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address piSession = address(0x5E5510); - address attestSession = address(0xD1FFE2); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", attestSession, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, piSession), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.SessionAddrMismatch.selector); - verifier.verifyAndExtract(payload); - } - - function test_honkVerifierFalse_reverts() public { - honk.setRet(false); - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - bytes memory payload = _buildPayload(bearerHash, nullifier, w, sessionAddr, "alice"); - vm.prank(w); - vm.expectRevert(XZkVerifier.ZkVerificationFailed.selector); - verifier.verifyAndExtract(payload); - } - - function test_handleNotFound_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - // Replace recvRevealed with something missing the handle. - meAttest.recvRevealed = bytes("{\"data\":{\"id\":\"42\"}}"); - // Re-sign with the tampered field. - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.me.v1"), - meAttest.bearerHash, - uint256(meAttest.bearerRangeStart), - uint256(meAttest.bearerRangeEnd), - keccak256(meAttest.sentRevealed), - uint256(meAttest.sentPrefixEnd), - uint256(meAttest.sentSuffixEnd), - keccak256(meAttest.recvRevealed), - keccak256(bytes(meAttest.handle)), - meAttest.sessionAddr, - uint256(meAttest.timestamp) - ) - ); - meAttest.notarySignature = _sign(NOTARY_PK, digest); - - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.HandleNotFound.selector); - verifier.verifyAndExtract(payload); - } - - function test_clientIdMismatch_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - // Replace sentRevealed with a body missing client_id. - tokenAttest.sentRevealed = bytes("grant_type=authorization_code&code=xyz"); - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.token.v1"), - tokenAttest.bearerHash, - uint256(tokenAttest.bearerRangeStart), - uint256(tokenAttest.bearerRangeEnd), - keccak256(tokenAttest.sentRevealed), - uint256(tokenAttest.timestamp) - ) - ); - tokenAttest.notarySignature = _sign(NOTARY_PK, digest); - - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.ClientIdMismatch.selector); - verifier.verifyAndExtract(payload); - } - - function test_meRequestPrefix_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - // Tamper sentRevealed: wrong method, but still ends with CRLF after - // the (would-be) bearer. Re-sign so digest matches. - bytes memory badPrefix = bytes("POST /wrong HTTP/1.1\r\nauthorization: Bearer "); - meAttest.sentRevealed = abi.encodePacked(badPrefix, bytes("\r\n")); - meAttest.sentPrefixEnd = uint32(badPrefix.length); - meAttest.bearerRangeStart = meAttest.sentPrefixEnd; - meAttest.sentSuffixEnd = meAttest.bearerRangeEnd + 2; - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.me.v1"), - meAttest.bearerHash, - uint256(meAttest.bearerRangeStart), - uint256(meAttest.bearerRangeEnd), - keccak256(meAttest.sentRevealed), - uint256(meAttest.sentPrefixEnd), - uint256(meAttest.sentSuffixEnd), - keccak256(meAttest.recvRevealed), - keccak256(bytes(meAttest.handle)), - meAttest.sessionAddr, - uint256(meAttest.timestamp) - ) - ); - meAttest.notarySignature = _sign(NOTARY_PK, digest); - - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - bytes memory payload = abi.encode(p); - - vm.prank(w); - vm.expectRevert(XZkVerifier.MeRequestPrefixMismatch.selector); - verifier.verifyAndExtract(payload); - } - - /// H1 — bearer-end CRLF anchor (revert paths). - /// - /// Builds a happy-path me attestation, then for each of the three new H1 - /// guards: corrupts one field, re-signs the notary digest with the new - /// field, asserts the contract reverts on the corresponding error. - /// All three guards are independent of A0.1; they prevent prover-side - /// bearer truncation by canonicalizing `bearer_len` via the trailing - /// CRLF byte pair. - - /// H1: trailing 2 bytes of sentRevealed must equal `\r\n`. - function test_meBearerEndNotCrlf_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - - // Replace the trailing CRLF with garbage; preserve length so layout - // and adjacency checks still pass — only `MeBearerEndNotCrlf` fires. - bytes memory sent = meAttest.sentRevealed; - sent[sent.length - 2] = 0x41; // 'A' - sent[sent.length - 1] = 0x42; // 'B' - meAttest.sentRevealed = sent; - meAttest.notarySignature = _signMe(meAttest); - - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - - vm.prank(w); - vm.expectRevert(XZkVerifier.MeBearerEndNotCrlf.selector); - verifier.verifyAndExtract(abi.encode(p)); - } - - /// H1: sentRevealed length must equal sentPrefixEnd + 2 (prefix bytes + - /// 2-byte CRLF trailer). Anything else = bad multi-range layout. - function test_meBadSentLayout_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - - // Append a third byte so layout != prefixLen + 2. - meAttest.sentRevealed = abi.encodePacked(meAttest.sentRevealed, bytes("X")); - meAttest.notarySignature = _signMe(meAttest); - - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - - vm.prank(w); - vm.expectRevert(XZkVerifier.MeBadSentLayout.selector); - verifier.verifyAndExtract(abi.encode(p)); - } - - /// H1: sentSuffixEnd MUST equal bearerRangeEnd + 2. Catches a prover - /// claiming a 2-byte tail that does not abut the bearer (e.g. CRLF taken - /// from a different line in the request). - function test_meBearerEndAdjacencyMismatch_reverts() public { - bytes32 bearerHash = bytes32(uint256(0xBEEF)); - bytes32 nullifier = bytes32(uint256(0xC0FFEE)); - address sessionAddr = address(0x5E5510); - address w = address(0x1111); - - XZkVerifier.TokenAttestation memory tokenAttest = _buildTokenAttest(bearerHash, uint64(block.timestamp)); - XZkVerifier.MeAttestation memory meAttest = - _buildMeAttest(bearerHash, "alice", sessionAddr, uint64(block.timestamp)); - - // Shift sentSuffixEnd one byte off canonical adjacency. - meAttest.sentSuffixEnd = meAttest.bearerRangeEnd + 3; - meAttest.notarySignature = _signMe(meAttest); - - XZkVerifier.XProof memory p = XZkVerifier.XProof({ - proof: hex"", - publicInputs: _buildPI(bearerHash, bearerHash, nullifier, w, sessionAddr), - tokenAttest: tokenAttest, - meAttest: meAttest - }); - - vm.prank(w); - vm.expectRevert(XZkVerifier.MeBearerEndAdjacencyMismatch.selector); - verifier.verifyAndExtract(abi.encode(p)); - } - - // ─── Helpers ───────────────────────────────────────────────────── - - /// Re-sign a me-attestation digest after one of its fields was mutated. - /// Mirrors `XZkVerifier._verifyMeSig` field order exactly. - function _signMe(XZkVerifier.MeAttestation memory att) internal view returns (bytes memory) { - bytes32 digest = keccak256( - abi.encode( - block.chainid, - address(verifier), - keccak256(bytes(PLATFORM)), - keccak256("XZkVerifier.me.v1"), - att.bearerHash, - uint256(att.bearerRangeStart), - uint256(att.bearerRangeEnd), - keccak256(att.sentRevealed), - uint256(att.sentPrefixEnd), - uint256(att.sentSuffixEnd), - keccak256(att.recvRevealed), - keccak256(bytes(att.handle)), - keccak256(bytes(att.userId)), - att.sessionAddr, - uint256(att.timestamp) - ) - ); - return _sign(NOTARY_PK, digest); - } -} diff --git a/solidity/archived/login/zk/test/XZkVerifierFlow.t.sol b/solidity/archived/login/zk/test/XZkVerifierFlow.t.sol deleted file mode 100644 index a360a2b7..00000000 --- a/solidity/archived/login/zk/test/XZkVerifierFlow.t.sol +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../Registry.sol"; -import {WalletFactory} from "../../WalletFactory.sol"; -import {WebWallet} from "../../WebWallet.sol"; -import {IZkSessionVerifier} from "../IZkSessionVerifier.sol"; -import {deployNotary} from "../../../notary/test/DeployNotary.sol"; - -/// Stand-in verifier that returns whatever the test author set up. Lets us -/// exercise the Registry dispatcher path without dragging in real -/// circuits / notary signatures (which live in `XZkVerifierBindings.t.sol`, -/// the X-specific cases). -contract StubZkVerifier is IZkSessionVerifier { - string public override platformName = "stub.example"; - string public handle = "alice"; - address public sessionKey = address(0xC0FFEE); - address public walletAddr = address(0); - uint256 public expiresAt = type(uint256).max; - bytes32 public nullifier = bytes32(uint256(0xBEEF)); - string public userId = "id_alice"; - bool public shouldRevert; - bytes public revertMessage; - - function setIdentity(string memory h, address k) external { - handle = h; - sessionKey = k; - // Escrow is keyed by the immutable id; derive a unique id per handle so - // distinct identities map to distinct wallets. - userId = string(abi.encodePacked("id_", h)); - } - - function setUserId(string memory u) external { - userId = u; - } - - function setWalletAddr(address w) external { - walletAddr = w; - } - - function setExpiresAt(uint256 e) external { - expiresAt = e; - } - - function setNullifier(bytes32 n) external { - nullifier = n; - } - - function setRevert(bool b, bytes memory m) external { - shouldRevert = b; - revertMessage = m; - } - - function verifyAndExtract(bytes calldata) - external - view - override - returns ( - string memory _handle, - address _sessionKey, - address _walletAddr, - uint256 _expiresAt, - bytes32 _nullifier, - string memory _userId - ) - { - if (shouldRevert) { - bytes memory m = revertMessage; - assembly { revert(add(m, 0x20), mload(m)) } - } - return (handle, sessionKey, walletAddr, expiresAt, nullifier, userId); - } -} - -/// Dispatcher-level tests for `Registry.register_session_zk(platform, bytes)`. -/// -/// The Registry stays small and platform-agnostic; per-platform proof -/// semantics (circuit, notary signature, attestation shape) live inside -/// each `IZkSessionVerifier` implementation. These tests cover the -/// Registry-side invariants — dispatch, expiry, session-key replay, -/// nullifier replay, and wallet-deploy idempotency — against a stub -/// verifier so the assertions don't depend on circuit specifics. -contract RegistryZkDispatcherTest is Test { - Registry registry; - WalletFactory factory; - StubZkVerifier verifier; - - address constant OWNER = address(0xA11CE); - address constant NOTARY = address(0xB0B); - address constant BACKEND = address(0xBE); - string constant PLATFORM = "api.x.com"; - - function setUp() public { - WebWallet walletImpl = new WebWallet(); - WalletFactory fImpl = new WalletFactory(); - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (OWNER, address(walletImpl), address(1))) - ) - ) - ); - - Registry rImpl = new Registry(); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall( - Registry.initialize, (address(deployNotary(OWNER, NOTARY)), BACKEND, address(factory), OWNER) - ) - ) - ) - ); - - vm.prank(OWNER); - factory.setRegistry(address(registry)); - - verifier = new StubZkVerifier(); - vm.prank(OWNER); - registry.setZkVerifier(PLATFORM, IZkSessionVerifier(address(verifier))); - } - - function test_happyPath_dispatchesAndRegistersHandle() public { - registry.register_session_zk(PLATFORM, hex""); - address wallet = registry.resolve(PLATFORM, "alice"); - assertTrue(wallet != address(0)); - // Session is keyed by the immutable (platform, userId), not the handle. - assertEq( - WebWallet(payable(wallet)).sessionAt(keccak256(abi.encode(PLATFORM, "id_alice")), 0), address(0xC0FFEE) - ); - } - - function test_unsetPlatform_reverts() public { - vm.expectRevert(Registry.ZkVerifierNotSet.selector); - registry.register_session_zk("not.configured", hex""); - } - - function test_expiredProof_reverts() public { - vm.warp(1_000_000); - verifier.setExpiresAt(1_000_000); // boundary: expiresAt == now → ProofExpired - vm.expectRevert(Registry.ProofExpired.selector); - registry.register_session_zk(PLATFORM, hex""); - } - - function test_zeroSessionKey_reverts() public { - verifier.setIdentity("alice", address(0)); - vm.expectRevert(bytes("zero session")); - registry.register_session_zk(PLATFORM, hex""); - } - - function test_sessionKeyReplay_reverts() public { - registry.register_session_zk(PLATFORM, hex""); - // second call with same session key (same handle, even) reverts on - // the global `usedSessionKeys` guard, NOT on handle-already-claimed. - verifier.setNullifier(bytes32(uint256(0xDEAD))); // unique nullifier - vm.expectRevert(Registry.SessionKeyAlreadyUsed.selector); - registry.register_session_zk(PLATFORM, hex""); - } - - function test_nullifierReplay_reverts() public { - registry.register_session_zk(PLATFORM, hex""); - // second call with a different session key but same nullifier - // reverts on the bearer-nullifier replay guard. - verifier.setIdentity("alice", address(0xC0FFEE2)); - vm.expectRevert(Registry.BearerNullifierUsed.selector); - registry.register_session_zk(PLATFORM, hex""); - } - - function test_verifierRevert_propagates() public { - verifier.setRevert(true, bytes("custom verifier rejection")); - vm.expectRevert(bytes("custom verifier rejection")); - registry.register_session_zk(PLATFORM, hex""); - } - - function test_secondHandleDeploysSecondWallet() public { - registry.register_session_zk(PLATFORM, hex""); - address w1 = registry.resolve(PLATFORM, "alice"); - - // Different session key + nullifier + handle → still the same - // platform; expected to deploy a NEW wallet (not link). - verifier.setIdentity("bob", address(0xB0B11E)); - verifier.setNullifier(bytes32(uint256(0xBAD2))); - registry.register_session_zk(PLATFORM, hex""); - address w2 = registry.resolve(PLATFORM, "bob"); - assertTrue(w2 != address(0)); - assertTrue(w2 != w1); - } - - function test_link_zk_happy_attachesHandleToCallingWallet() public { - registry.register_session_zk(PLATFORM, hex""); - address wallet = registry.resolve(PLATFORM, "alice"); - - verifier.setIdentity("alice2", address(0xA11CE2)); - verifier.setWalletAddr(wallet); // PI walletAddress == msg.sender for link - verifier.setNullifier(bytes32(uint256(0x1111))); - vm.prank(wallet); - registry.link_identity_zk(PLATFORM, hex""); - - assertEq(registry.resolve(PLATFORM, "alice2"), wallet); - } - - function test_link_zk_unregisteredCaller_reverts() public { - address stranger = address(0x5727A6); - verifier.setIdentity("bob", address(0xB0B5E5)); - verifier.setWalletAddr(stranger); // matches msg.sender, so we hit NotRegisteredWallet - verifier.setNullifier(bytes32(uint256(0x3333))); - vm.prank(stranger); - vm.expectRevert(Registry.NotRegisteredWallet.selector); - registry.link_identity_zk(PLATFORM, hex""); - } - - /// Register path must reject any non-zero walletAddress in the proof's PI. - /// The factory deploys a fresh wallet, so binding is not allowed. - function test_register_zk_nonZeroWalletAddr_reverts() public { - verifier.setWalletAddr(address(0xDEADBEEF)); - vm.expectRevert(Registry.WrongWalletAddr.selector); - registry.register_session_zk(PLATFORM, hex""); - } - - /// Link path: PI walletAddress must equal msg.sender. Substituting a - /// different wallet (the classic mempool front-run vector) reverts. - function test_link_zk_walletAddr_mismatch_reverts() public { - // Bootstrap alice's wallet. - registry.register_session_zk(PLATFORM, hex""); - address aliceWallet = registry.resolve(PLATFORM, "alice"); - - // Try to link a second handle from a DIFFERENT wallet's pov. - address otherWallet = address(0xB0B0B0B0); - verifier.setIdentity("alice2", address(0xA11CE2)); - verifier.setWalletAddr(aliceWallet); // proof's PI binds to aliceWallet - verifier.setNullifier(bytes32(uint256(0x4242))); - - vm.prank(otherWallet); - vm.expectRevert(Registry.WrongWalletAddr.selector); - registry.link_identity_zk(PLATFORM, hex""); - } - - /// Link path: PI walletAddress == 0 (register-path shape) must NOT be - /// accepted as a link, even from a registered wallet. - function test_link_zk_zeroWalletAddr_reverts() public { - registry.register_session_zk(PLATFORM, hex""); - address aliceWallet = registry.resolve(PLATFORM, "alice"); - - verifier.setIdentity("alice2", address(0xA11CE2)); - verifier.setWalletAddr(address(0)); // register-shape PI - verifier.setNullifier(bytes32(uint256(0x5757))); - - vm.prank(aliceWallet); - vm.expectRevert(Registry.WrongWalletAddr.selector); - registry.link_identity_zk(PLATFORM, hex""); - } -} diff --git a/solidity/archived/notary/INotary.sol b/solidity/archived/notary/INotary.sol deleted file mode 100644 index 99ad8b32..00000000 --- a/solidity/archived/notary/INotary.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title INotary — the one notary check every contract shares. -/// -/// @notice A consumer computes its own digest (its domain separation — chain -/// id, its own address, operation tags — stays its own concern) and -/// asks this contract whether `proof` attests to it. Today a proof is -/// a secp256k1 signature over the EIP-191-prefixed digest; a future -/// implementation may accept a different kind of proof entirely, which -/// is why the parameter is opaque bytes rather than a signature type. -interface INotary { - /// @notice True when `proof` is a valid notary attestation of `digest`. - /// @param digest The consumer-computed digest being attested (pre-EIP-191; - /// the implementation owns whatever wrapping its proof kind needs). - /// @param proof Opaque attestation bytes. V1: a 65-byte ECDSA signature. - /// @dev MUST return false rather than revert on a malformed `proof`, so a - /// consumer can surface its own error. - function verify(bytes32 digest, bytes calldata proof) external view returns (bool); - - /// @notice The current notary signer. Observability for V1's key scheme; a - /// proof-based implementation may return the zero address. - function notary() external view returns (address); -} diff --git a/solidity/archived/notary/Notary.sol b/solidity/archived/notary/Notary.sol deleted file mode 100644 index b9a89c31..00000000 --- a/solidity/archived/notary/Notary.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -import {INotary} from "./INotary.sol"; - -/// @title Notary — the single notary-attestation verifier. -/// -/// @notice Every contract that accepts notary attestations holds a pointer to -/// this proxy and calls `verify(digest, proof)`. The digest is the -/// caller's own (each consumer keeps its domain separation exactly -/// where it was); what lives HERE is the one thing they all shared: -/// which notary is trusted, and how an attestation is checked. -/// -/// @dev V1 stores a single signer address and checks a secp256k1 signature -/// over the EIP-191-prefixed digest. Rotation is `setNotary`; a change of -/// proof system is a UUPS upgrade of this contract alone — consumers keep -/// calling the same `verify(bytes32, bytes)`. -/// -/// Upgrade authority: owner (intended to be a multisig / governance). -contract Notary is INotary, Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { - /// @inheritdoc INotary - address public notary; - - event NotaryChanged(address indexed previousNotary, address indexed newNotary); - - error ZeroAddress(); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /// @param owner_ Address that will own the contract (should be a multisig). - /// @param notary_ The initial notary signer. - function initialize(address owner_, address notary_) external initializer { - __Ownable_init(owner_); - __Ownable2Step_init(); - __UUPSUpgradeable_init(); - _setNotary(notary_); - } - - /// @notice Rotate the notary signer. - function setNotary(address notary_) external onlyOwner { - _setNotary(notary_); - } - - /// @inheritdoc INotary - /// @dev EIP-191 prefix + ECDSA recover + compare. `tryRecover` rejects - /// wrong-length, malleable (high-s), and unrecoverable signatures by - /// returning an error, so a malformed proof answers false rather than - /// reverting — the consumer surfaces its own error. - function verify(bytes32 digest, bytes calldata proof) external view returns (bool) { - bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(digest); - (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(ethHash, proof); - return err == ECDSA.RecoverError.NoError && recovered == notary; - } - - function _setNotary(address notary_) internal { - if (notary_ == address(0)) revert ZeroAddress(); - emit NotaryChanged(notary, notary_); - notary = notary_; - } - - /// @dev Required by UUPS — only the owner can upgrade the implementation. - function _authorizeUpgrade(address) internal override onlyOwner {} - - /// @dev Renouncing would leave no way to rotate the notary or upgrade. - function renounceOwnership() public pure override { - revert("renounce disabled"); - } -} diff --git a/solidity/archived/notary/test/DeployNotary.sol b/solidity/archived/notary/test/DeployNotary.sol deleted file mode 100644 index 20023247..00000000 --- a/solidity/archived/notary/test/DeployNotary.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -import {Notary} from "../Notary.sol"; - -/// Test-only helper: a Notary behind a fresh ERC1967 proxy, initialized with -/// `owner_` and `signer_`. Every suite that needs a notary check wires one of -/// these, so the wiring lives in one place. -function deployNotary(address owner_, address signer_) returns (Notary) { - Notary impl = new Notary(); - return Notary(address(new ERC1967Proxy(address(impl), abi.encodeCall(Notary.initialize, (owner_, signer_))))); -} diff --git a/solidity/archived/notary/test/Notary.t.sol b/solidity/archived/notary/test/Notary.t.sol deleted file mode 100644 index 54926b16..00000000 --- a/solidity/archived/notary/test/Notary.t.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -import {Notary} from "../Notary.sol"; - -contract NotaryTest is Test { - Notary internal notaryContract; - - uint256 internal constant SIGNER_PK = 0xA11CE; - uint256 internal constant OTHER_PK = 0xB0B; - - address internal owner = makeAddr("owner"); - address internal stranger = makeAddr("stranger"); - address internal signer; - - event NotaryChanged(address indexed previousNotary, address indexed newNotary); - - function setUp() public { - signer = vm.addr(SIGNER_PK); - Notary impl = new Notary(); - notaryContract = - Notary(address(new ERC1967Proxy(address(impl), abi.encodeCall(Notary.initialize, (owner, signer))))); - } - - /// EIP-191 signature over `digest`, the way the notary signs. - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - // ─── initialize ───────────────────────────────────────────────── - - function test_initialize_setsOwnerAndNotary() public view { - assertEq(notaryContract.owner(), owner); - assertEq(notaryContract.notary(), signer); - } - - function test_initialize_revertsOnZeroNotary() public { - Notary impl = new Notary(); - vm.expectRevert(Notary.ZeroAddress.selector); - new ERC1967Proxy(address(impl), abi.encodeCall(Notary.initialize, (owner, address(0)))); - } - - function test_initialize_revertsOnDoubleInit() public { - vm.expectRevert(); - notaryContract.initialize(owner, signer); - } - - // ─── verify ───────────────────────────────────────────────────── - - function test_verify_acceptsTheNotarySignature() public view { - bytes32 digest = keccak256("some consumer digest"); - assertTrue(notaryContract.verify(digest, _sign(SIGNER_PK, digest))); - } - - function test_verify_rejectsAnotherSigner() public view { - bytes32 digest = keccak256("some consumer digest"); - assertFalse(notaryContract.verify(digest, _sign(OTHER_PK, digest))); - } - - function test_verify_rejectsASignatureOverAnotherDigest() public view { - bytes memory sig = _sign(SIGNER_PK, keccak256("digest A")); - assertFalse(notaryContract.verify(keccak256("digest B"), sig)); - } - - /// A malformed proof answers false rather than reverting, so a consumer - /// surfaces its own error. - function test_verify_answersFalseOnMalformedProof() public view { - bytes32 digest = keccak256("some consumer digest"); - assertFalse(notaryContract.verify(digest, "")); - assertFalse(notaryContract.verify(digest, hex"deadbeef")); - assertFalse(notaryContract.verify(digest, new bytes(65))); - } - - /// A high-s (malleable) variant of a valid signature is refused. - function test_verify_rejectsAMalleatedSignature() public view { - bytes32 digest = keccak256("some consumer digest"); - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, ethHash); - // secp256k1 n - uint256 n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; - bytes memory malleated = abi.encodePacked(r, bytes32(n - uint256(s)), v == 27 ? uint8(28) : uint8(27)); - assertFalse(notaryContract.verify(digest, malleated)); - } - - // ─── setNotary ────────────────────────────────────────────────── - - function test_setNotary_rotatesAndEmits() public { - address rotated = vm.addr(OTHER_PK); - vm.prank(owner); - vm.expectEmit(true, true, false, false); - emit NotaryChanged(signer, rotated); - notaryContract.setNotary(rotated); - assertEq(notaryContract.notary(), rotated); - - // The old key stops verifying; the new one starts. - bytes32 digest = keccak256("some consumer digest"); - assertFalse(notaryContract.verify(digest, _sign(SIGNER_PK, digest))); - assertTrue(notaryContract.verify(digest, _sign(OTHER_PK, digest))); - } - - function test_setNotary_revertsIfNotOwner() public { - vm.prank(stranger); - vm.expectRevert(); - notaryContract.setNotary(stranger); - } - - function test_setNotary_revertsOnZeroAddress() public { - vm.prank(owner); - vm.expectRevert(Notary.ZeroAddress.selector); - notaryContract.setNotary(address(0)); - } - - // ─── Ownable2Step ─────────────────────────────────────────────── - - function test_ownership_twoStepTransfer() public { - address newOwner = makeAddr("newOwner"); - vm.prank(owner); - notaryContract.transferOwnership(newOwner); - assertEq(notaryContract.owner(), owner); - assertEq(notaryContract.pendingOwner(), newOwner); - - vm.prank(newOwner); - notaryContract.acceptOwnership(); - assertEq(notaryContract.owner(), newOwner); - } - - function test_renounceOwnership_isDisabled() public { - vm.prank(owner); - vm.expectRevert("renounce disabled"); - notaryContract.renounceOwnership(); - } - - // ─── UUPS upgrade ─────────────────────────────────────────────── - - function test_upgrade_revertsIfNotOwner() public { - Notary newImpl = new Notary(); - vm.prank(stranger); - vm.expectRevert(); - notaryContract.upgradeToAndCall(address(newImpl), ""); - } - - function test_upgrade_byOwner_preservesState() public { - Notary newImpl = new Notary(); - vm.prank(owner); - notaryContract.upgradeToAndCall(address(newImpl), ""); - - assertEq(notaryContract.owner(), owner); - assertEq(notaryContract.notary(), signer); - bytes32 digest = keccak256("some consumer digest"); - assertTrue(notaryContract.verify(digest, _sign(SIGNER_PK, digest))); - } -} diff --git a/solidity/archived/transfer/MockERC20.sol b/solidity/archived/transfer/MockERC20.sol deleted file mode 100644 index fe621837..00000000 --- a/solidity/archived/transfer/MockERC20.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title MockERC20 — test token with public mint. -contract MockERC20 is ERC20 { - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) {} - - /// @notice Anyone can mint — test only. - function mint(address to, uint256 amount) external { - _mint(to, amount); - } -} diff --git a/solidity/archived/transfer/bank/BankErrors.sol b/solidity/archived/transfer/bank/BankErrors.sol deleted file mode 100644 index f00b64a0..00000000 --- a/solidity/archived/transfer/bank/BankErrors.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -// ── Shared Bank custom errors ───────────────────────────────────────────── -// File-level errors imported by the Bank facets + logic libraries. Replaces the -// monolith's mix of `error` declarations and `require("string")` reverts with a -// single, cheap (4-byte selector) error set. Library-private errors (e.g. -// LibString's parse errors) stay in their library. - -// Admin / config -error ZeroAddress(); -error EmptyName(); -error AlreadyRegistered(); -error NotRegistered(); - -// Token registry -error TokenNotRegistered(); - -// Template matching -error TemplateNotFound(); -error NoTemplateMatch(); // no template matched with the correct recipient/amount/token -error TemplateMissingToken(); // template has {amount} but no {token} -error TemplateMissingAmount(); // template has {recipient} but no {amount} -error TemplateMissingTokenAfterRecipient(); // template has {recipient}/{amount} but no {token} - -// Proof / signatures -error InvalidNotarySignature(); -error InvalidBackendSignature(); -error InvalidMerkleProof(); -error MerklePathTooLong(); -error DomainHashMismatch(); - -// Recipient binding (reply-inferred / quote-inferred honor) -error RecipientLessNeedsReplyTarget(); -error ReplyLeafTooShort(); -error ReplyLeafNotInReplyTo(); -error ReplyTargetMismatch(); -error ReplyTargetIdNotQuoted(); -error QuotedRefTooShort(); -error QuotedRefNotObject(); -error QuotedRefNotFlatObject(); -error RefNotQuote(); -error QuotedRefMissingId(); -error QuotedAuthorLeafTooShort(); -error QuotedAuthorLeafNotIncludes(); -error QuotedAuthorMissingQuotedId(); -error QuotedAuthorMissingAuthorId(); -error QuotedAuthorDifferentObject(); -error QuotedAuthorMismatch(); -error EmptyQuotedValue(); // an extracted "…" reply-target / quoted id / author-id value is empty - -// Transfer / escrow -error TransferAlreadyUsed(); -error ZeroAmount(); -error InsufficientBalance(); -error SenderNotRegistered(); -error UserIdRequired(); -error UnknownResourceType(); -error RequestPathMismatch(); -error AmountWeiMismatch(); -error ZeroToken(); -error AuthorNotInRevealedField(); - -// Native asset -error NativeTransferFailed(); -error NativeAmountMismatch(); -error NativeSentWithErc20(); - -// Allowance -error InsufficientAllowance(address token, address owner, address spender, uint256 required, uint256 actual); - -// Source URL -error SourceUrlNotHttps(); -error SourceUrlDomainMismatch(); - -// Lifecycle -error EnforcedPause(); -error ReentrantCall(); -error AlreadyInitialized(); diff --git a/solidity/archived/transfer/bank/BankInit.sol b/solidity/archived/transfer/bank/BankInit.sol deleted file mode 100644 index ae7d3ac0..00000000 --- a/solidity/archived/transfer/bank/BankInit.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -import {LibDiamond} from "../diamond/libraries/LibDiamond.sol"; -import {IDiamondCut} from "../diamond/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../diamond/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../diamond/interfaces/IERC173.sol"; - -import {LibCoreStorage} from "./storage/LibCoreStorage.sol"; -import {LibTemplateStorage} from "./storage/LibTemplateStorage.sol"; -import {LibTemplate} from "./libraries/LibTemplate.sol"; -import {ZeroAddress, AlreadyInitialized} from "./BankErrors.sol"; - -/// @title BankInit — one-shot diamond initializer (run via diamondCut delegatecall). -/// @dev Runs in the diamond's storage context: sets the trusted-party pointers, -/// primes the reentrancy guard, registers ERC-165 interface ids, and seeds -/// the honor templates + resource/web prefixes (mirrors the old -/// `Bank.initialize`). Fresh deploy — no legacy state to preserve. -contract BankInit { - function init(address notary, address backend, address registry) external { - if (notary == address(0) || backend == address(0) || registry == address(0)) revert ZeroAddress(); - - LibCoreStorage.CoreStorage storage cs = LibCoreStorage.store(); - // One-shot: a later owner diamondCut re-delegatecalling init would otherwise - // silently overwrite the trusted-party pointers and append duplicate templates. - if (cs.initialized) revert AlreadyInitialized(); - cs.initialized = true; - cs.notary = notary; - cs.backend = backend; - cs.registry = registry; - cs.reentrancyStatus = 1; // not-entered (nonReentrant treats 2 as entered) - - LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); - ds.supportedInterfaces[type(IERC165).interfaceId] = true; - ds.supportedInterfaces[type(IDiamondCut).interfaceId] = true; - ds.supportedInterfaces[type(IDiamondLoupe).interfaceId] = true; - ds.supportedInterfaces[type(IERC173).interfaceId] = true; - - // Honor templates (mirror the old Bank.initialize). - LibTemplate.addTemplate("api.github.com", "@dyaka-agent honor @{recipient} with {amount} of {token}"); - LibTemplate.addTemplate("api.github.com", "@dyaka-agent honor @{recipient} with {amount} {token}"); - LibTemplate.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} of {token}"); - LibTemplate.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} {token}"); - // Recipient-less variants (recipient inferred off-chain from the reply/quote - // parent). X only: GitHub reply-target id leaves aren't supplied yet, so - // advertising the syntax on-chain would only revert. Phase 2. - LibTemplate.addTemplate("api.x.com", "@dyaka_agent honor with {amount} of {token}"); - LibTemplate.addTemplate("api.x.com", "@dyaka_agent honor with {amount} {token}"); - - LibTemplateStorage.TemplateStorage storage tls = LibTemplateStorage.store(); - tls.resourceTypePrefixes[keccak256("issue_comment")] = "/repos/"; - tls.resourceTypePrefixes[keccak256("pr_comment")] = "/repos/"; - tls.resourceTypePrefixes[keccak256("pr_review")] = "/repos/"; - tls.resourceTypePrefixes[keccak256("issue_body")] = "/repos/"; - tls.resourceTypePrefixes[keccak256("discussion_comment")] = "/graphql"; - tls.resourceTypePrefixes[keccak256("tweet")] = "/2/tweets"; - - tls.platformWebPrefixes[keccak256("api.github.com")] = "https://github.com/"; - tls.platformWebPrefixes[keccak256("api.x.com")] = "https://x.com/"; - } -} diff --git a/solidity/archived/transfer/bank/BankModifiers.sol b/solidity/archived/transfer/bank/BankModifiers.sol deleted file mode 100644 index 8f8525ac..00000000 --- a/solidity/archived/transfer/bank/BankModifiers.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibDiamond} from "../diamond/libraries/LibDiamond.sol"; -import {LibCoreStorage} from "./storage/LibCoreStorage.sol"; -import {EnforcedPause, ReentrantCall} from "./BankErrors.sol"; - -/// @title BankModifiers — shared access / lifecycle modifiers for Bank facets. -/// @dev One definition inherited by every facet (a diamond can't inherit OZ's -/// Ownable/Pausable/ReentrancyGuard per-facet). Owner is the diamond owner -/// (ERC-173); pause + reentrancy flags live in LibCoreStorage. -abstract contract BankModifiers { - modifier onlyOwner() { - LibDiamond.enforceIsContractOwner(); - _; - } - - modifier whenNotPaused() { - if (LibCoreStorage.store().paused) revert EnforcedPause(); - _; - } - - modifier nonReentrant() { - LibCoreStorage.CoreStorage storage cs = LibCoreStorage.store(); - if (cs.reentrancyStatus == 2) revert ReentrantCall(); - cs.reentrancyStatus = 2; - _; - cs.reentrancyStatus = 1; - } -} diff --git a/solidity/archived/transfer/bank/BankTypes.sol b/solidity/archived/transfer/bank/BankTypes.sol deleted file mode 100644 index 97d81fab..00000000 --- a/solidity/archived/transfer/bank/BankTypes.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title BankTypes — shared calldata structs for the Bank facets + logic libs. -/// @dev The honor-transfer call surface (resource / sender / backend-sig / TLS -/// proof). Extracted from the monolith so facets and `LibProof` / -/// `LibRecipient` reference one definition. - -struct ResourceInfo { - string platform; // API domain (e.g. "api.x.com", "api.github.com") - string resourceType; - string resourceId; - string requestPath; -} - -/// `revealedAuthorId` present → id parsed on-chain, id-keyed; empty → handle path. -struct SenderInfo { - string author; - bytes revealedAuthor; - bytes revealedAuthorId; -} - -struct BackendSig { - bytes sig; - uint256 timestamp; -} - -struct NotaryTlsProof { - bytes notarySig; - bytes32 domainHash; - bytes32 clientRandom; - bytes32 serverRandom; - bytes serverEphemeralKey; - bytes32 transcriptRoot; - uint256 timestamp; - bytes32[] bodyMerklePath; - bytes32[] authorMerklePath; - bytes32[] domainMerklePath; - bytes32[] endpointMerklePath; - bytes32[] authorIdMerklePath; // empty for the legacy handle path - bytes32[] receiverIdMerklePath; // recipient-less reply honor: reply-target id leaf - bytes revealedReceiverId; // revealed "in_reply_to_user_id":"" snippet - bytes32[] quotedRefMerklePath; // recipient-less quote honor: referenced_tweets quoted leaf - bytes revealedQuotedRef; // revealed {"type":"quoted","id":""} snippet - bytes32[] quotedAuthorMerklePath; // recipient-less quote honor: includes quoted-tweet author leaf - bytes revealedQuotedAuthorId; // revealed includes span binding "id":"" + "author_id":"" -} diff --git a/solidity/archived/transfer/bank/IBank.sol b/solidity/archived/transfer/bank/IBank.sol deleted file mode 100644 index eccf7ac1..00000000 --- a/solidity/archived/transfer/bank/IBank.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "./BankTypes.sol"; - -/// @title IBank — the Bank diamond's full external surface (Vault + Admin + Transfer -/// facets), for callers that address the diamond as one contract (tests, -/// scripts, the frontend). Structs come from `BankTypes`. -interface IBank { - // ── Vault ──────────────────────────────────────────────────────────── - function deposit( - string calldata platform, - string calldata handle, - string calldata userId, - address token, - uint256 amount - ) external payable; - function withdraw(address recipient, address token, uint256 amount) external; - function balanceOf(address wallet, address token) external view returns (uint256); - function balanceOf(string calldata platform, string calldata handle, string calldata userId, address token) - external - view - returns (uint256); - function balanceOfTotal(address wallet, address token) external view returns (uint256); - function balanceOfTotal(string calldata platform, string calldata handle, string calldata userId, address token) - external - view - returns (uint256); - function registeredBalances(address wallet, address token) external view returns (uint256); - function unregisteredBalances(bytes32 key, address token) external view returns (uint256); - function identityHash(string calldata domain, string calldata username) external pure returns (bytes32); - - // ── Admin ──────────────────────────────────────────────────────────── - function pause() external; - function unpause() external; - function setRegistry(address registry) external; - function registerToken(string calldata name, address token) external; - function unregisterToken(string calldata name) external; - function resolveToken(string calldata name) external view returns (address); - function getRegisteredTokens() external view returns (address[] memory); - function setPlatformTemplate(string calldata platform, string calldata newTemplate) external; - function clearPlatformTemplates(string calldata platform) external; - function platformTemplateCount(string calldata platform) external view returns (uint256); - function getPlatformTemplate(string calldata platform, uint256 index) external view returns (string memory); - function getTemplateParts(string calldata platform, uint256 index) - external - view - returns (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix); - function setResourceTypePrefix(string calldata resourceType, string calldata prefix) external; - function setPlatformWebPrefix(string calldata platform, string calldata prefix) external; - function getPlatformWebPrefix(string calldata platform) external view returns (string memory); - function registry() external view returns (address); - function paused() external view returns (bool); - function tokenName(address token) external view returns (string memory); - - // ── Transfer ───────────────────────────────────────────────────────── - function verifyProof( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverUserId - ) external view returns (bool); - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - string calldata tokenName_, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external; - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external; - function transfer_within( - string calldata receiverPlatform, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - uint256 amount - ) external; -} diff --git a/solidity/archived/transfer/bank/facets/AdminFacet.sol b/solidity/archived/transfer/bank/facets/AdminFacet.sol deleted file mode 100644 index a6d7670c..00000000 --- a/solidity/archived/transfer/bank/facets/AdminFacet.sol +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {BankModifiers} from "../BankModifiers.sol"; -import {LibCoreStorage} from "../storage/LibCoreStorage.sol"; -import {LibTokenStorage} from "../storage/LibTokenStorage.sol"; -import {LibTemplateStorage} from "../storage/LibTemplateStorage.sol"; -import {LibTemplate} from "../libraries/LibTemplate.sol"; -import {LibString} from "../libraries/LibString.sol"; -import {ZeroAddress, EmptyName, AlreadyRegistered, NotRegistered} from "../BankErrors.sol"; - -/// @title AdminFacet — owner-gated Bank configuration. -/// @dev Token registry, honor-template admin, resource-type / web prefixes, the -/// Registry pointer, and pause. Reads/writes the token/template/core storage -/// concerns; owner is the diamond owner (via BankModifiers). -contract AdminFacet is BankModifiers { - event Paused(address account); - event Unpaused(address account); - event RegistrySet(address indexed oldRegistry, address indexed newRegistry); - event TokenRegistered(string name, address indexed token); - event TokenUnregistered(string name, address indexed token); - - // ── Lifecycle ──────────────────────────────────────────────────────── - - /// @notice Pause state-changing entrypoints (emergency stop / migration gate). - function pause() external onlyOwner { - LibCoreStorage.store().paused = true; - emit Paused(msg.sender); - } - - /// @notice Resume after a pause. - function unpause() external onlyOwner { - LibCoreStorage.store().paused = false; - emit Unpaused(msg.sender); - } - - // ── Registry pointer ───────────────────────────────────────────────── - - function setRegistry(address _registry) external onlyOwner { - if (_registry == address(0)) revert ZeroAddress(); - LibCoreStorage.CoreStorage storage cs = LibCoreStorage.store(); - address old = cs.registry; - cs.registry = _registry; - if (old != _registry) emit RegistrySet(old, _registry); - } - - // ── Token registry ─────────────────────────────────────────────────── - - function registerToken(string calldata name, address token) external onlyOwner { - if (bytes(name).length == 0) revert EmptyName(); - LibTokenStorage.TokenStorage storage ts = LibTokenStorage.store(); - bytes32 key = _nameKey(name); - // Name taken? `tokenByName` covers ERC-20s, but the NATIVE asset keeps - // address(0) there (indistinguishable from unset), so a native-registered - // name would slip through — also reject a name already held by the native - // (else registering it for an ERC-20 hijacks the name / desyncs the registry). - if (ts.tokenByName[key] != address(0)) revert AlreadyRegistered(); - // Case-INSENSITIVE for the native: webTransferV2 resolves the native asset - // via LibString.caseInsensitiveEqual, so a case variant (native "TIA" then - // ERC-20 "tia") must be rejected here too — else the registry desyncs and a - // by-name honor can settle the wrong asset. - if ( - bytes(ts.tokenName[address(0)]).length != 0 - && LibString.caseInsensitiveEqual(ts.tokenName[address(0)], name) - ) { - revert AlreadyRegistered(); - } - // Address already registered (blocks a second name for the same token → dup - // entry, and a second native registration). - if (bytes(ts.tokenName[token]).length != 0) revert AlreadyRegistered(); - ts.tokenByName[key] = token; - ts.tokenName[token] = name; - ts.registeredTokens.push(token); - emit TokenRegistered(name, token); - } - - function unregisterToken(string calldata name) external onlyOwner { - LibTokenStorage.TokenStorage storage ts = LibTokenStorage.store(); - bytes32 key = _nameKey(name); - address token = ts.tokenByName[key]; - // token == address(0) is ambiguous: the native asset registered under - // `name`, or a name that was never registered. It is the native ONLY when - // the stored native name matches `name` — otherwise revert (don't wipe it). - if (token == address(0)) { - string memory nativeName = ts.tokenName[address(0)]; - if (bytes(nativeName).length == 0 || _nameKey(nativeName) != key) revert NotRegistered(); - } - delete ts.tokenByName[key]; - delete ts.tokenName[token]; - _removeRegisteredToken(ts, token); - emit TokenUnregistered(name, token); - } - - function resolveToken(string calldata name) external view returns (address) { - return LibTokenStorage.store().tokenByName[_nameKey(name)]; - } - - function getRegisteredTokens() external view returns (address[] memory) { - return LibTokenStorage.store().registeredTokens; - } - - /// @dev Token-name storage key. NB: case-sensitive — the webTransferV2 token - /// lookup is likewise case-sensitive; unifying them case-insensitively is - /// blocked by webTransferV2's stack limit (needs a param-struct refactor). - function _nameKey(string memory name) private pure returns (bytes32) { - return keccak256(bytes(name)); - } - - /// @dev Swap-pop `token` out of the registeredTokens list (no-op if absent). - function _removeRegisteredToken(LibTokenStorage.TokenStorage storage ts, address token) private { - uint256 n = ts.registeredTokens.length; - for (uint256 i = 0; i < n; i++) { - if (ts.registeredTokens[i] == token) { - ts.registeredTokens[i] = ts.registeredTokens[n - 1]; - ts.registeredTokens.pop(); - return; - } - } - } - - // ── Template admin ─────────────────────────────────────────────────── - - /// @notice Add a template for a platform (appends to the list). - function setPlatformTemplate(string calldata platform, string calldata newTemplate) external onlyOwner { - LibTemplate.addTemplate(platform, newTemplate); - } - - /// @notice Remove all templates for a platform. - function clearPlatformTemplates(string calldata platform) external onlyOwner { - LibTemplateStorage.TemplateStorage storage ts = LibTemplateStorage.store(); - bytes32 key = keccak256(bytes(platform)); - delete ts.platformTemplates[key]; - delete ts.templateParts[key]; - } - - function platformTemplateCount(string calldata platform) external view returns (uint256) { - return LibTemplateStorage.store().platformTemplates[keccak256(bytes(platform))].length; - } - - /// @notice The raw stored template string at `index` (verbatim, unparsed). - function getPlatformTemplate(string calldata platform, uint256 index) external view returns (string memory) { - return LibTemplateStorage.store().platformTemplates[keccak256(bytes(platform))][index]; - } - - function getTemplateParts(string calldata platform, uint256 index) - external - view - returns (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) - { - LibTemplateStorage.TemplateParts storage parts = - LibTemplateStorage.store().templateParts[keccak256(bytes(platform))][index]; - return (parts.prefix, parts.afterRecipient, parts.afterAmount, parts.suffix); - } - - // ── Resource / web prefixes ────────────────────────────────────────── - - function setResourceTypePrefix(string calldata resourceType, string calldata prefix) external onlyOwner { - LibTemplateStorage.store().resourceTypePrefixes[keccak256(bytes(resourceType))] = prefix; - } - - function setPlatformWebPrefix(string calldata platform, string calldata prefix) external onlyOwner { - LibTemplateStorage.store().platformWebPrefixes[keccak256(bytes(platform))] = prefix; - } - - /// @notice The web-URL prefix bound to a platform ("" if unset). - function getPlatformWebPrefix(string calldata platform) external view returns (string memory) { - return LibTemplateStorage.store().platformWebPrefixes[keccak256(bytes(platform))]; - } - - // ── Views (parity with the old Bank public state) ───────────────────── - - /// @notice The Registry pointer used for identity resolution. - function registry() external view returns (address) { - return LibCoreStorage.store().registry; - } - - /// @notice Whether state-changing entrypoints are paused. - function paused() external view returns (bool) { - return LibCoreStorage.store().paused; - } - - /// @notice The registered name for a token address ("" if unregistered). - function tokenName(address token) external view returns (string memory) { - return LibTokenStorage.store().tokenName[token]; - } -} diff --git a/solidity/archived/transfer/bank/facets/TransferFacet.sol b/solidity/archived/transfer/bank/facets/TransferFacet.sol deleted file mode 100644 index 67956c06..00000000 --- a/solidity/archived/transfer/bank/facets/TransferFacet.sol +++ /dev/null @@ -1,333 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; - -import {BankModifiers} from "../BankModifiers.sol"; -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "../BankTypes.sol"; -import {IRegistry} from "../../../login/IRegistry.sol"; - -import {LibProof} from "../libraries/LibProof.sol"; -import {LibTemplate} from "../libraries/LibTemplate.sol"; -import {LibRecipient} from "../libraries/LibRecipient.sol"; -import {LibEscrow} from "../libraries/LibEscrow.sol"; -import {LibString} from "../libraries/LibString.sol"; - -import {LibCoreStorage} from "../storage/LibCoreStorage.sol"; -import {LibEscrowStorage} from "../storage/LibEscrowStorage.sol"; -import {LibTemplateStorage} from "../storage/LibTemplateStorage.sol"; -import {LibTokenStorage} from "../storage/LibTokenStorage.sol"; - -import { - UnknownResourceType, - RequestPathMismatch, - AuthorNotInRevealedField, - TemplateNotFound, - TokenNotRegistered, - ZeroToken, - AmountWeiMismatch, - TransferAlreadyUsed, - ZeroAmount, - SenderNotRegistered, - SourceUrlNotHttps, - SourceUrlDomainMismatch -} from "../BankErrors.sol"; - -/// @title TransferFacet — the honor-transfer flow (proof → verify → move funds). -/// @dev Orchestrates the logic libs: `LibProof` (notary + backend + Merkle leaves), -/// `LibTemplate` (command match), `LibRecipient` (recipient-less binding), and -/// `LibEscrow` (id resolve + ledgers). Fresh deploy is id-only — the sender -/// is resolved from the proven author-id, the receiver from the proven -/// recipient id; handles are display-only (event fields). The two -/// `webTransferV2` overloads (token by-name / by-address) share `_webTransfer`. -contract TransferFacet is BankModifiers { - event WebTransferV3( - address indexed senderWallet, - address indexed receiverWallet, - string receiverPlatform, - string receiverHandle, - string receiverUserId, - address indexed token, - uint256 amount, - string sourceUrl - ); - event TransferWithin( - address indexed sender, - string receiverPlatform, - string receiverHandle, - string receiverUserId, - address indexed token, - uint256 amount - ); - - // ── Preflight ──────────────────────────────────────────────────────── - - /// @notice Verify a TLS-notarized comment proof with no state changes. Reverts - /// with a descriptive error on any failure. Returns true when valid. - function verifyProof( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverUserId - ) public view returns (bool) { - bytes32 uid = _uid(resource); - - string memory prefix = LibTemplateStorage.store().resourceTypePrefixes[keccak256(bytes(resource.resourceType))]; - if (bytes(prefix).length == 0) revert UnknownResourceType(); - if (!LibString.contains(bytes(resource.requestPath), bytes(prefix), false)) revert RequestPathMismatch(); - - LibProof.verifyNotarySignature(notaryTlsProof); - LibProof.verifyRecvLeaf(notaryTlsProof.bodyMerklePath, notaryTlsProof.transcriptRoot, revealedSubsection); - LibProof.verifyRecvLeaf(notaryTlsProof.authorMerklePath, notaryTlsProof.transcriptRoot, sender.revealedAuthor); - LibProof.verifyDomainLeaf( - notaryTlsProof.domainMerklePath, notaryTlsProof.transcriptRoot, resource.platform, notaryTlsProof.domainHash - ); - LibProof.verifyEndpointLeaf( - notaryTlsProof.endpointMerklePath, notaryTlsProof.transcriptRoot, resource.requestPath - ); - if (!LibString.contains(sender.revealedAuthor, bytes(sender.author), false)) revert AuthorNotInRevealedField(); - - // id-only: the author id is always proven and parsed at resolve time. - LibProof.verifyRecvLeaf( - notaryTlsProof.authorIdMerklePath, notaryTlsProof.transcriptRoot, sender.revealedAuthorId - ); - - LibProof.verifyBackendSignature( - uid, - revealedSubsection, - sender.revealedAuthor, - sender.revealedAuthorId, - receiverUserId, - backendSigData.sig, - backendSigData.timestamp - ); - - bytes32 platformKey = keccak256(bytes(resource.platform)); - if (LibTemplateStorage.store().platformTemplates[platformKey].length == 0) revert TemplateNotFound(); - (bool matched,,, bool recipientLess) = LibTemplate.matchAnyTemplate(revealedSubsection, platformKey); - if (!matched) revert TemplateNotFound(); - - // Recipient-less honor: the body does not prove the recipient, so preflight - // must apply the SAME reply-target binding that webTransferV2 enforces — - // else verifyProof would green-light a proof the transfer then reverts. - if (recipientLess) { - LibRecipient.verifyRecipientIdLeaf(notaryTlsProof, receiverUserId); - } - - return true; - } - - /// @dev The transfer uid: keccak of platform:resourceType:resourceId. - function _uid(ResourceInfo calldata resource) private pure returns (bytes32) { - return keccak256(abi.encodePacked(resource.platform, ":", resource.resourceType, ":", resource.resourceId)); - } - - // ── Web transfer (comment proof → funds) ───────────────────────────── - - /// @notice Execute a web transfer (token by name) with source URL. - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - string calldata tokenName_, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external nonReentrant whenNotPaused { - _validateSourceUrl(resource, sourceUrl); - - LibTokenStorage.TokenStorage storage tks = LibTokenStorage.store(); - address token = tks.tokenByName[keccak256(bytes(tokenName_))]; - if (token == address(0)) { - if (!LibString.caseInsensitiveEqual(tks.tokenName[address(0)], tokenName_)) revert TokenNotRegistered(); - } - - // Arg-binding: reverts unless a template matches these recipient/amount/token. - // Its recipientLess result is AUTHORITATIVE and must gate the reply/quote - // binding here — verifyProof's matchAnyTemplate can classify differently when - // a literal template matches first, which would skip the binding. - if (LibTemplate.verifyTemplate(revealedSubsection, resource.platform, receiverHandle, amountStr, tokenName_)) { - LibRecipient.verifyRecipientIdLeaf(notaryTlsProof, receiverUserId); - } - - (address senderWallet, address receiverWallet) = _webTransfer( - resource, - sender, - revealedSubsection, - notaryTlsProof, - backendSigData, - receiverUserId, - token, - amountStr, - amount - ); - emit WebTransferV3( - senderWallet, receiverWallet, resource.platform, receiverHandle, receiverUserId, token, amount, sourceUrl - ); - } - - /// @notice Execute a web transfer (token by address) with source URL. - function webTransferV2( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - string calldata amountStr, - uint256 amount, - string calldata sourceUrl - ) external nonReentrant whenNotPaused { - _validateSourceUrl(resource, sourceUrl); - if (token == address(0)) revert ZeroToken(); - // Gate the web-transfer surface to registered tokens (parity with the - // by-name overload + VaultFacet.deposit): verifyTemplate matches on the hex - // address, so without this an unregistered ERC-20 would be pulled into the - // ledger via autoTopUp. - if (bytes(LibTokenStorage.store().tokenName[token]).length == 0) revert TokenNotRegistered(); - - // Arg-binding (see the by-name overload): its recipientLess result gates - // the reply/quote binding — authoritative over verifyProof's matchAnyTemplate. - if (LibTemplate.verifyTemplate( - revealedSubsection, resource.platform, receiverHandle, amountStr, Strings.toHexString(token) - )) { - LibRecipient.verifyRecipientIdLeaf(notaryTlsProof, receiverUserId); - } - - (address senderWallet, address receiverWallet) = _webTransfer( - resource, - sender, - revealedSubsection, - notaryTlsProof, - backendSigData, - receiverUserId, - token, - amountStr, - amount - ); - emit WebTransferV3( - senderWallet, receiverWallet, resource.platform, receiverHandle, receiverUserId, token, amount, sourceUrl - ); - } - - /// @dev Shared body of both `webTransferV2` overloads: amount-wei check, proof - /// verification, and the balance move. The recipient-less binding + token - /// resolution differ by overload and stay there. - function _webTransfer( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverUserId, - address token, - string calldata amountStr, - uint256 amount - ) internal returns (address senderWallet, address receiverWallet) { - if (amount != LibString.parseDecimalAmount(amountStr, LibEscrow.tokenDecimals(token))) revert AmountWeiMismatch(); - - bytes32 uid = - _verifyAndGetUid(resource, sender, revealedSubsection, notaryTlsProof, backendSigData, receiverUserId); - (senderWallet, receiverWallet) = _executeTransfer(uid, resource.platform, sender, receiverUserId, token, amount); - } - - // ── Transfer within the Bank (WebWallet → WebWallet) ───────────────── - - /// @notice Transfer between internal Bank balances without touching the - /// chain. Caller must be a registered WebWallet. Lazy-migrates + - /// auto-tops-up. - function transfer_within( - string calldata receiverPlatform, - string calldata receiverHandle, - string calldata receiverUserId, - address token, - uint256 amount - ) external nonReentrant whenNotPaused { - if (amount == 0) revert ZeroAmount(); - - (string[] memory platforms,) = IRegistry(LibCoreStorage.store().registry).getHandles(msg.sender); - if (platforms.length == 0) revert SenderNotRegistered(); - - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - LibEscrow.migrate(msg.sender, token); - LibEscrow.autoTopUp(msg.sender, token, amount); - es.registeredBalances[msg.sender][token] -= amount; - - address receiverWallet = LibEscrow.resolveById(receiverPlatform, receiverUserId); - if (receiverWallet != address(0)) { - LibEscrow.migrate(receiverWallet, token); - es.registeredBalances[receiverWallet][token] += amount; - } else { - es.unregisteredBalances[LibEscrow.escrowKey(receiverPlatform, receiverUserId)][token] += amount; - } - - emit TransferWithin(msg.sender, receiverPlatform, receiverHandle, receiverUserId, token, amount); - } - - // ── Internals ──────────────────────────────────────────────────────── - - /// @dev Verify proof and compute UID. Reverts if invalid or already used. - function _verifyAndGetUid( - ResourceInfo calldata resource, - SenderInfo calldata sender, - bytes calldata revealedSubsection, - NotaryTlsProof calldata notaryTlsProof, - BackendSig calldata backendSigData, - string calldata receiverUserId - ) internal view returns (bytes32 uid) { - uid = _uid(resource); - if (LibCoreStorage.store().usedTransfers[uid]) revert TransferAlreadyUsed(); - verifyProof(resource, sender, revealedSubsection, notaryTlsProof, backendSigData, receiverUserId); - } - - /// @dev Validate caller-provided sourceUrl: https + platform web domain. Empty - /// sourceUrl skips validation. - function _validateSourceUrl(ResourceInfo calldata resource, string calldata sourceUrl) internal view { - if (bytes(sourceUrl).length == 0) return; - if (!LibString.startsWith(bytes(sourceUrl), bytes("https://"))) revert SourceUrlNotHttps(); - - string memory webPrefix = LibTemplateStorage.store().platformWebPrefixes[keccak256(bytes(resource.platform))]; - if (bytes(webPrefix).length > 0 && !LibString.startsWith(bytes(sourceUrl), bytes(webPrefix))) { - revert SourceUrlDomainMismatch(); - } - } - - /// @dev Move funds after verification: deduct sender, credit receiver or escrow, - /// mark uid used. Both parties resolved by proven id (recycle-proof). - function _executeTransfer( - bytes32 uid, - string calldata platform, - SenderInfo calldata sender, - string calldata receiverUserId, - address token, - uint256 amount - ) internal returns (address senderWallet, address receiverWallet) { - if (amount == 0) revert ZeroAmount(); - - senderWallet = LibEscrow.resolveById(platform, LibString.extractId(sender.revealedAuthorId)); - if (senderWallet == address(0)) revert SenderNotRegistered(); - - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - LibEscrow.migrate(senderWallet, token); - LibEscrow.autoTopUp(senderWallet, token, amount); - es.registeredBalances[senderWallet][token] -= amount; - - receiverWallet = LibEscrow.resolveById(platform, receiverUserId); - if (receiverWallet != address(0)) { - LibEscrow.migrate(receiverWallet, token); - es.registeredBalances[receiverWallet][token] += amount; - } else { - es.unregisteredBalances[LibEscrow.escrowKey(platform, receiverUserId)][token] += amount; - } - - LibCoreStorage.store().usedTransfers[uid] = true; - } -} diff --git a/solidity/archived/transfer/bank/facets/VaultFacet.sol b/solidity/archived/transfer/bank/facets/VaultFacet.sol deleted file mode 100644 index 98dfa0af..00000000 --- a/solidity/archived/transfer/bank/facets/VaultFacet.sol +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {BankModifiers} from "../BankModifiers.sol"; -import {LibEscrow} from "../libraries/LibEscrow.sol"; -import {LibEscrowStorage} from "../storage/LibEscrowStorage.sol"; -import {LibTokenStorage} from "../storage/LibTokenStorage.sol"; -import { - ZeroAmount, - TokenNotRegistered, - NativeAmountMismatch, - NativeSentWithErc20, - InsufficientAllowance, - InsufficientBalance, - NativeTransferFailed -} from "../BankErrors.sol"; - -/// @title VaultFacet — deposits, withdrawals, and balance views. -/// @dev The user-facing money surface. Funds live in the diamond; the ledgers + -/// id-escrow live in LibEscrowStorage, read/written through LibEscrow. -contract VaultFacet is BankModifiers { - using SafeERC20 for IERC20; - - event Deposited(address indexed wallet, bytes32 indexed handleKey, address indexed token, uint256 amount); - event Withdrawn(address indexed wallet, address indexed recipient, address indexed token, uint256 amount); - - /// @notice Top up a user's balance. Credits the resolved wallet, or id-escrow - /// when the recipient hasn't registered yet. - function deposit( - string calldata platform, - string calldata handle, - string calldata userId, - address token, - uint256 amount - ) external payable nonReentrant whenNotPaused { - if (amount == 0) revert ZeroAmount(); - if (bytes(LibTokenStorage.store().tokenName[token]).length == 0) revert TokenNotRegistered(); - - if (token == address(0)) { - if (msg.value != amount) revert NativeAmountMismatch(); - } else { - if (msg.value != 0) revert NativeSentWithErc20(); - uint256 allowed = IERC20(token).allowance(msg.sender, address(this)); - if (allowed < amount) revert InsufficientAllowance(token, msg.sender, address(this), amount, allowed); - IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - } - - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - address wallet = LibEscrow.resolveById(platform, userId); - if (wallet != address(0)) { - LibEscrow.migrate(wallet, token); - es.registeredBalances[wallet][token] += amount; - emit Deposited(wallet, bytes32(0), token, amount); - } else { - bytes32 key = LibEscrow.escrowKey(platform, userId); - es.unregisteredBalances[key][token] += amount; - emit Deposited(address(0), key, token, amount); - } - } - - /// @notice Withdraw from the caller's registered balance to `recipient`. - function withdraw(address recipient, address token, uint256 amount) external nonReentrant whenNotPaused { - if (amount == 0) revert ZeroAmount(); - - LibEscrow.migrate(msg.sender, token); - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - if (es.registeredBalances[msg.sender][token] < amount) revert InsufficientBalance(); - es.registeredBalances[msg.sender][token] -= amount; - - if (token == address(0)) { - (bool ok,) = recipient.call{value: amount}(""); - if (!ok) revert NativeTransferFailed(); - } else { - IERC20(token).safeTransfer(recipient, amount); - } - emit Withdrawn(msg.sender, recipient, token, amount); - } - - /// @notice Registered + id-escrow balance (Bank-held) for a wallet. - function balanceOf(address wallet, address token) external view returns (uint256) { - return LibEscrow.totalBalance(wallet, token); - } - - /// @notice Bank-held balance plus the wallet's own on-chain token balance. - function balanceOfTotal(address wallet, address token) external view returns (uint256) { - uint256 walletBal = token == address(0) ? wallet.balance : IERC20(token).balanceOf(wallet); - return LibEscrow.totalBalance(wallet, token) + walletBal; - } - - /// @notice Bank-held balance for a (platform, id) identity. Registered → the - /// full aggregated total; else the id-escrow slot. Handle is display-only. - function balanceOf(string calldata platform, string calldata handle, string calldata userId, address token) - external - view - returns (uint256) - { - address wallet = LibEscrow.resolveById(platform, userId); - if (wallet != address(0)) return LibEscrow.totalBalance(wallet, token); - if (bytes(userId).length == 0) return 0; - return LibEscrowStorage.store().unregisteredBalances[LibEscrow.escrowKey(platform, userId)][token]; - } - - /// @notice As the (platform,...) `balanceOf`, plus the resolved wallet's own - /// on-chain token balance. - function balanceOfTotal(string calldata platform, string calldata handle, string calldata userId, address token) - external - view - returns (uint256) - { - address wallet = LibEscrow.resolveById(platform, userId); - if (wallet == address(0)) { - if (bytes(userId).length == 0) return 0; - return LibEscrowStorage.store().unregisteredBalances[LibEscrow.escrowKey(platform, userId)][token]; - } - uint256 walletBal = token == address(0) ? wallet.balance : IERC20(token).balanceOf(wallet); - return LibEscrow.totalBalance(wallet, token) + walletBal; - } - - /// @notice Compute the identity hash for a (domain, username) pair. - function identityHash(string calldata domain, string calldata username) external pure returns (bytes32) { - return LibEscrow.identityHash(domain, username); - } - - /// @notice Raw registered-ledger balance for a wallet (matches the old Bank - /// public mapping getter). Aggregation is `balanceOf`. - function registeredBalances(address wallet, address token) external view returns (uint256) { - return LibEscrowStorage.store().registeredBalances[wallet][token]; - } - - /// @notice Raw id-escrow slot balance (matches the old Bank public mapping getter). - function unregisteredBalances(bytes32 key, address token) external view returns (uint256) { - return LibEscrowStorage.store().unregisteredBalances[key][token]; - } -} diff --git a/solidity/archived/transfer/bank/libraries/LibEscrow.sol b/solidity/archived/transfer/bank/libraries/LibEscrow.sol deleted file mode 100644 index 4af5f610..00000000 --- a/solidity/archived/transfer/bank/libraries/LibEscrow.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {LibEscrowStorage} from "../storage/LibEscrowStorage.sol"; -import {LibCoreStorage} from "../storage/LibCoreStorage.sol"; -import {IRegistry} from "../../../login/IRegistry.sol"; -import {UserIdRequired, InsufficientBalance, InsufficientAllowance} from "../BankErrors.sol"; - -/// @title LibEscrow — the shared money layer (ledgers + id-escrow) for the Bank -/// vault and transfer facets. -/// @dev Escrow is keyed by the immutable platform userId (recycle-proof), -/// domain-separated with ID_ESCROW_TAG. On a FRESH deploy there is no legacy -/// handle-keyed escrow, so sweeps + totals read the id keyspace only. -library LibEscrow { - using SafeERC20 for IERC20; - - /// @dev Domain-separation tag for the id-keyed escrow keyspace. - string internal constant ID_ESCROW_TAG = "id:v1"; - - /// @dev Escrow slot key, keyed by the immutable platform userId. - function escrowKey(string memory platform, string memory userId) internal pure returns (bytes32) { - if (bytes(userId).length == 0) revert UserIdRequired(); - return keccak256(abi.encode(ID_ESCROW_TAG, platform, userId)); - } - - /// @dev Identity hash for a (domain, username) pair. - function identityHash(string calldata domain, string calldata username) internal pure returns (bytes32) { - return keccak256(abi.encode(domain, username)); - } - - /// @dev Resolve a wallet by its immutable platform userId. Fresh deploy has no - /// pre-id (handle-keyed) users, so there is no handle fallback — a miss is - /// "unregistered" and the caller escrows under the id. - function resolveById(string memory platform, string memory id) internal view returns (address) { - return IRegistry(LibCoreStorage.store().registry).resolveById(platform, id); - } - - /// @dev Token decimals (native defaults to 18). A genuine 0-decimal ERC-20 is - /// honored as 0 — a token whose `decimals()` reverts fails the whole tx. - function tokenDecimals(address token) internal view returns (uint8) { - if (token == address(0)) return 18; - return IERC20Metadata(token).decimals(); - } - - /// @dev Sweep one id-escrow slot into the wallet's registered balance. - function sweepKey(address wallet, address token, bytes32 key) internal { - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - uint256 pending = es.unregisteredBalances[key][token]; - if (pending > 0) { - delete es.unregisteredBalances[key][token]; - es.registeredBalances[wallet][token] += pending; - } - } - - /// @dev Sweep the wallet's id-escrow slots into its registered balance (the - /// ids are proven via the Registry, so recycle-proof). - function migrate(address wallet, address token) internal { - (string[] memory platforms, string[] memory userIds) = - IRegistry(LibCoreStorage.store().registry).getUserIds(wallet); - for (uint256 i = 0; i < platforms.length; i++) { - if (bytes(userIds[i]).length == 0) continue; - sweepKey(wallet, token, keccak256(abi.encode(ID_ESCROW_TAG, platforms[i], userIds[i]))); - } - } - - /// @dev View total: registered + all (id-keyed) unregistered escrow for wallet. - function totalBalance(address wallet, address token) internal view returns (uint256 total) { - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - total = es.registeredBalances[wallet][token]; - (string[] memory platforms, string[] memory userIds) = - IRegistry(LibCoreStorage.store().registry).getUserIds(wallet); - for (uint256 i = 0; i < platforms.length; i++) { - if (bytes(userIds[i]).length == 0) continue; - total += es.unregisteredBalances[keccak256(abi.encode(ID_ESCROW_TAG, platforms[i], userIds[i]))][token]; - } - } - - /// @dev Ensure `wallet` has `amount` registered, pulling any shortfall via its - /// ERC-20 allowance (native tokens can't be pulled → revert on shortfall). - function autoTopUp(address wallet, address token, uint256 amount) internal { - LibEscrowStorage.EscrowStorage storage es = LibEscrowStorage.store(); - uint256 bal = es.registeredBalances[wallet][token]; - if (bal < amount) { - if (token == address(0)) revert InsufficientBalance(); - uint256 shortfall = amount - bal; - uint256 allowed = IERC20(token).allowance(wallet, address(this)); - if (allowed < shortfall) revert InsufficientAllowance(token, wallet, address(this), shortfall, allowed); - IERC20(token).safeTransferFrom(wallet, address(this), shortfall); - es.registeredBalances[wallet][token] += shortfall; - } - } -} diff --git a/solidity/archived/transfer/bank/libraries/LibProof.sol b/solidity/archived/transfer/bank/libraries/LibProof.sol deleted file mode 100644 index 07a996cd..00000000 --- a/solidity/archived/transfer/bank/libraries/LibProof.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; - -import {LibCoreStorage} from "../storage/LibCoreStorage.sol"; -import {NotaryTlsProof} from "../BankTypes.sol"; -import { - InvalidNotarySignature, - InvalidBackendSignature, - InvalidMerkleProof, - MerklePathTooLong, - DomainHashMismatch -} from "../BankErrors.sol"; -import {INotary} from "../../../notary/INotary.sol"; - -/// @title LibProof — TLS-notary proof + backend-signature verification. -/// @dev Merkle-leaf membership (recv / domain / endpoint), the notary attestation -/// signature, and the backend authorization signature. Reads trusted-party -/// pointers from `LibCoreStorage`; ECDSA/Merkle via OpenZeppelin. -library LibProof { - /// @dev The shared leaf verifier: reject an over-long path, hash the - /// double-keccak leaf from `preimage`, and check Merkle membership. - /// Every leaf kind differs only in its `preimage` (a `prefix`+data blob). - function verifyLeaf(bytes32[] calldata path, bytes32 root, bytes memory preimage) internal pure { - if (path.length > 32) revert MerklePathTooLong(); - bytes32 leaf = keccak256(bytes.concat(keccak256(preimage))); - if (!MerkleProof.verify(path, root, leaf)) revert InvalidMerkleProof(); - } - - /// @dev A recv-transcript leaf (`"recv:"` + revealed bytes) — used for the - /// body, author, author-id, reply-target, and quote leaves. - function verifyRecvLeaf(bytes32[] calldata path, bytes32 root, bytes calldata data) internal pure { - verifyLeaf(path, root, abi.encodePacked("recv:", data)); - } - - /// @dev The domain leaf; also binds `domain` to the proof's `expectedDomainHash`. - function verifyDomainLeaf(bytes32[] calldata path, bytes32 root, string calldata domain, bytes32 expectedDomainHash) - internal - pure - { - if (keccak256(bytes(domain)) != expectedDomainHash) revert DomainHashMismatch(); - verifyLeaf(path, root, abi.encodePacked("domain:", domain)); - } - - /// @dev The endpoint (request-path) leaf. - function verifyEndpointLeaf(bytes32[] calldata path, bytes32 root, string calldata endpoint) internal pure { - verifyLeaf(path, root, abi.encodePacked("endpoint:", endpoint)); - } - - /// @dev Verify the notary attestation over the TLS-session digest. - /// Binds `block.chainid` + the current `registry` so a proof can't cross - /// chains or survive a Registry migration. The shared Notary contract - /// (`cs.notary`) owns the attestation check itself. - function verifyNotarySignature(NotaryTlsProof calldata proof) internal view { - LibCoreStorage.CoreStorage storage cs = LibCoreStorage.store(); - bytes32 digest = keccak256( - abi.encode( - block.chainid, - cs.registry, - proof.domainHash, - proof.clientRandom, - proof.serverRandom, - keccak256(proof.serverEphemeralKey), - proof.transcriptRoot, - proof.timestamp - ) - ); - if (!INotary(cs.notary).verify(digest, proof.notarySig)) revert InvalidNotarySignature(); - } - - /// @dev Verify the backend authorization signature. `receiverUserId` is signed - /// (hashed, to stay unambiguous next to the variable-length byte fields) - /// so a relayer can't redirect credit to another id. - function verifyBackendSignature( - bytes32 uid, - bytes calldata revealedSubsection, - bytes calldata revealedAuthor, - bytes calldata revealedAuthorId, - string calldata receiverUserId, - bytes calldata sig, - uint256 timestamp - ) internal view { - // Each of the three dynamic byte fields is independently Merkle-verified - // against transcriptRoot (verifyRecvLeaf on body/author/authorId), so a - // boundary re-split can't be forged — plain encodePacked is safe here. - bytes32 digest = keccak256( - abi.encodePacked( - uid, revealedSubsection, revealedAuthor, revealedAuthorId, keccak256(bytes(receiverUserId)), timestamp - ) - ); - address signer = ECDSA.recover(MessageHashUtils.toEthSignedMessageHash(digest), sig); - if (signer != LibCoreStorage.store().backend) revert InvalidBackendSignature(); - } -} diff --git a/solidity/archived/transfer/bank/libraries/LibRecipient.sol b/solidity/archived/transfer/bank/libraries/LibRecipient.sol deleted file mode 100644 index cb546cf4..00000000 --- a/solidity/archived/transfer/bank/libraries/LibRecipient.sol +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibString} from "./LibString.sol"; -import {LibProof} from "./LibProof.sol"; -import {NotaryTlsProof} from "../BankTypes.sol"; -import { - RecipientLessNeedsReplyTarget, - ReplyLeafTooShort, - ReplyLeafNotInReplyTo, - ReplyTargetMismatch, - ReplyTargetIdNotQuoted, - QuotedRefTooShort, - QuotedRefNotObject, - QuotedRefNotFlatObject, - RefNotQuote, - QuotedRefMissingId, - QuotedAuthorLeafTooShort, - QuotedAuthorLeafNotIncludes, - QuotedAuthorMissingQuotedId, - QuotedAuthorMissingAuthorId, - QuotedAuthorDifferentObject, - QuotedAuthorMismatch, - EmptyQuotedValue -} from "../BankErrors.sol"; - -/// @title LibRecipient — recipient binding for the recipient-less honor form. -/// @dev The body proves no `{recipient}`, so the recipient comes from a notarized -/// reply target (`in_reply_to_user_id`) or the quoted tweet's author. Each -/// binding is pinned to its specific field so a proof generator can't reveal -/// an arbitrary transcript id. Uses `LibProof.verifyRecvLeaf` for Merkle -/// membership and `LibString` for structural checks / value extraction. -library LibRecipient { - /// @dev Reply leaf takes precedence; otherwise a quote binding is required. - function verifyRecipientIdLeaf(NotaryTlsProof calldata proof, string calldata receiverUserId) internal pure { - if (proof.revealedReceiverId.length > 0) { - verifyReplyTargetLeaf(proof, receiverUserId); - return; - } - if (proof.revealedQuotedRef.length == 0) revert RecipientLessNeedsReplyTarget(); - verifyQuotedAuthorLeaf(proof, receiverUserId); - } - - /// @dev Reply binding: the revealed `"in_reply_to_user_id":""` leaf, with - /// its id equal to the backend-signed `receiverUserId`. Pinned to the - /// reply-target field (not "some id in the transcript"). - function verifyReplyTargetLeaf(NotaryTlsProof calldata proof, string calldata receiverUserId) internal pure { - bytes calldata leaf = proof.revealedReceiverId; - bytes memory key = bytes('"in_reply_to_user_id":'); - if (leaf.length < key.length) revert ReplyLeafTooShort(); - for (uint256 i = 0; i < key.length; i++) { - if (leaf[i] != key[i]) revert ReplyLeafNotInReplyTo(); - } - LibProof.verifyRecvLeaf(proof.receiverIdMerklePath, proof.transcriptRoot, leaf); - if (keccak256(bytes(extractReplyTargetId(leaf, key.length))) != keccak256(bytes(receiverUserId))) { - revert ReplyTargetMismatch(); - } - } - - /// @dev Quote binding: recipient = author of the quoted tweet, joined across - /// two notarized leaves so the bound id is unambiguously the quoted - /// author (not the sender, a mention, or a reply parent): - /// - `revealedQuotedRef` — a flat `{…}` `referenced_tweets` object - /// containing `"type":"quoted"` and the quoted parent's `"id":""` - /// (field order not assumed). - /// - `revealedQuotedAuthorId` — an `includes.tweets` slice STARTING with - /// `"tweets":[{` (so its first `author_id` is a real includes-tweet - /// author, not the sender's `data.author_id`) that contains the join - /// `"id":""` in the SAME object as the extracted `author_id`. - /// Require the extracted author == `receiverUserId`. - function verifyQuotedAuthorLeaf(NotaryTlsProof calldata proof, string calldata receiverUserId) internal pure { - bytes calldata refLeaf = proof.revealedQuotedRef; - if (refLeaf.length < 2) revert QuotedRefTooShort(); - if (refLeaf[0] != "{" || refLeaf[refLeaf.length - 1] != "}") revert QuotedRefNotObject(); - for (uint256 i = 1; i + 1 < refLeaf.length; i++) { - if (refLeaf[i] == "{" || refLeaf[i] == "}") revert QuotedRefNotFlatObject(); - } - if (!LibString.contains(refLeaf, bytes('"type":"quoted"'), false)) revert RefNotQuote(); - LibProof.verifyRecvLeaf(proof.quotedRefMerklePath, proof.transcriptRoot, refLeaf); - (bool hasQid, uint256 qidPos) = LibString.indexOf(refLeaf, bytes('"id":"'), 0, false); - if (!hasQid) revert QuotedRefMissingId(); - string memory qid = _readQuotedValue(refLeaf, qidPos + 6); // '"id":"'.length == 6 - - bytes calldata authorLeaf = proof.revealedQuotedAuthorId; - bytes memory tweetsKey = bytes('"tweets":[{'); - if (authorLeaf.length <= tweetsKey.length) revert QuotedAuthorLeafTooShort(); - for (uint256 i = 0; i < tweetsKey.length; i++) { - if (authorLeaf[i] != tweetsKey[i]) revert QuotedAuthorLeafNotIncludes(); - } - LibProof.verifyRecvLeaf(proof.quotedAuthorMerklePath, proof.transcriptRoot, authorLeaf); - - // Join: the leaf must contain the quoted tweet object (id == qid). - (bool hasId, uint256 idPos) = LibString.indexOf(authorLeaf, abi.encodePacked('"id":"', qid, '"'), 0, false); - if (!hasId) revert QuotedAuthorMissingQuotedId(); - // Bind the extracted author to the SAME includes-tweet object as the qid - // join: no `}` (object close) between the `author_id` and the qid match. - (bool hasAuthor, uint256 authorPos) = LibString.indexOf(authorLeaf, bytes('"author_id":"'), 0, false); - if (!hasAuthor) revert QuotedAuthorMissingAuthorId(); - uint256 lo = authorPos < idPos ? authorPos : idPos; - uint256 hi = authorPos < idPos ? idPos : authorPos; - for (uint256 i = lo; i < hi; i++) { - if (authorLeaf[i] == "}") revert QuotedAuthorDifferentObject(); - } - string memory rid = _readQuotedValue(authorLeaf, authorPos + 13); // '"author_id":"'.length == 13 - if (keccak256(bytes(rid)) != keccak256(bytes(receiverUserId))) revert QuotedAuthorMismatch(); - } - - /// @dev The `in_reply_to_user_id` value, anchored at `prefixLen` (the length of - /// the already-verified `"in_reply_to_user_id":` prefix). Requires the - /// opening quote, so a trailing colon-separated id can't override it. - function extractReplyTargetId(bytes calldata leaf, uint256 prefixLen) internal pure returns (string memory) { - if (prefixLen >= leaf.length || leaf[prefixLen] != '"') revert ReplyTargetIdNotQuoted(); - return _readQuotedValue(leaf, prefixLen + 1); - } - - /// @dev Read a `"…"` value: from `valueStart` up to the next `"`. The shared - /// extraction behind all three id fields. Reverts on an empty value. - function _readQuotedValue(bytes calldata leaf, uint256 valueStart) internal pure returns (string memory) { - uint256 ve = valueStart; - while (ve < leaf.length && leaf[ve] != '"') ve++; - if (ve <= valueStart) revert EmptyQuotedValue(); - return string(leaf[valueStart:ve]); - } -} diff --git a/solidity/archived/transfer/bank/libraries/LibString.sol b/solidity/archived/transfer/bank/libraries/LibString.sol deleted file mode 100644 index a025228c..00000000 --- a/solidity/archived/transfer/bank/libraries/LibString.sol +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title LibString — Bank-specific byte/string primitives NOT covered by OZ. -/// @dev Only what OpenZeppelin doesn't provide: multi-byte substring search -/// (OZ `Bytes.indexOf` is single-byte), case-insensitive compare/search -/// (OZ `Strings.equal` is case-sensitive), token-char classification, tight -/// author-id parsing, and decimal-amount scaling. For the rest, callers use -/// OZ directly: `Strings.toHexString(address)` and `Bytes.slice(...)` for a -/// memory substring. require-strings replaced with custom errors. -library LibString { - error InvalidRange(); - error MalformedAuthorId(); - error EmptyAuthorId(); - error MultipleDecimalPoints(); - - /// @dev Find `needle` in memory `haystack` from `from` (case-sensitive). - /// @dev The single substring-search primitive. Find `needle` in `haystack` - /// from `from`; `ci` = ASCII case-insensitive. Memory haystack unifies - /// the memory/calldata variants — a calldata arg is copied in, and the - /// found position is identical byte-for-byte, so a caller may slice its - /// ORIGINAL calldata by the returned position. `(true, from)` for an - /// empty needle. - function indexOf(bytes memory haystack, bytes memory needle, uint256 from, bool ci) - internal - pure - returns (bool, uint256) - { - if (needle.length == 0) return (true, from); - if (from + needle.length > haystack.length) return (false, 0); - uint256 searchLen = haystack.length - needle.length + 1; - for (uint256 i = from; i < searchLen; i++) { - bool found = true; - for (uint256 j = 0; j < needle.length; j++) { - bytes1 h = haystack[i + j]; - bytes1 n = needle[j]; - if (ci ? toLower(h) != toLower(n) : h != n) { - found = false; - break; - } - } - if (found) return (true, i); - } - return (false, 0); - } - - /// @dev Substring of calldata bytes `[start, end)`. (For a MEMORY substring - /// use OZ `Bytes.slice` — this calldata variant is native slicing.) - function substringCalldata(bytes calldata data, uint256 start, uint256 end) internal pure returns (string memory) { - if (end < start || end > data.length) revert InvalidRange(); - return string(data[start:end]); - } - - /// @dev Case-insensitive string equality. - function caseInsensitiveEqual(string memory a, string memory b) internal pure returns (bool) { - bytes memory ab = bytes(a); - bytes memory bb = bytes(b); - if (ab.length != bb.length) return false; - for (uint256 i = 0; i < ab.length; i++) { - if (toLower(ab[i]) != toLower(bb[i])) return false; - } - return true; - } - - /// @dev Whether `haystack` contains `needle`; `ci` = case-insensitive. - /// Thin wrapper over [`indexOf`]. - function contains(bytes memory haystack, bytes memory needle, bool ci) internal pure returns (bool) { - (bool found,) = indexOf(haystack, needle, 0, ci); - return found; - } - - /// @dev Whether `haystack` begins with `prefix` (case-sensitive). Distinct from - /// `contains` — the match must be at position 0. - function startsWith(bytes memory haystack, bytes memory prefix) internal pure returns (bool) { - if (haystack.length < prefix.length) return false; - for (uint256 i = 0; i < prefix.length; i++) { - if (haystack[i] != prefix[i]) return false; - } - return true; - } - - /// @dev Parse id from a tight author-id snippet: quoted `"k":""` or bare - /// `"k":[,}]`. Value is after the LAST colon. - function extractId(bytes calldata s) internal pure returns (string memory) { - uint256 n = s.length; - uint256 c = n; - while (c > 0) { - c--; - if (s[c] == ":") break; - } - if (c >= n || s[c] != ":") revert MalformedAuthorId(); - uint256 i = c + 1; - bool quoted = i < n && s[i] == '"'; - if (quoted) i++; - uint256 start = i; - if (quoted) { - while (i < n && s[i] != '"') i++; - } else { - while (i < n && s[i] >= 0x30 && s[i] <= 0x39) i++; - } - if (i <= start) revert EmptyAuthorId(); - return string(s[start:i]); - } - - /// @dev Valid token-name character: a-z, A-Z, 0-9, `$`, `_`, `.`. - function isTokenChar(bytes1 c) internal pure returns (bool) { - return (c >= 0x41 && c <= 0x5A) // A-Z - || (c >= 0x61 && c <= 0x7A) // a-z - || (c >= 0x30 && c <= 0x39) // 0-9 - || c == "$" || c == "_" || c == "."; - } - - /// @dev ASCII lower-casing of a single byte. - function toLower(bytes1 b) internal pure returns (bytes1) { - if (b >= 0x41 && b <= 0x5A) { - return bytes1(uint8(b) + 32); - } - return b; - } - - /// @dev Human decimal amount (e.g. "0.05", "100") → wei at `decimals`. - /// Digits beyond `decimals` precision are truncated. - function parseDecimalAmount(string memory s, uint8 decimals) internal pure returns (uint256) { - bytes memory b = bytes(s); - uint256 intPart = 0; - uint256 fracPart = 0; - uint8 fracDigits = 0; - bool inFrac = false; - for (uint256 i = 0; i < b.length; i++) { - if (b[i] == ".") { - if (inFrac) revert MultipleDecimalPoints(); - inFrac = true; - } else if (b[i] >= "0" && b[i] <= "9") { - if (!inFrac) { - intPart = intPart * 10 + uint256(uint8(b[i]) - 48); - } else if (fracDigits < decimals) { - fracPart = fracPart * 10 + uint256(uint8(b[i]) - 48); - fracDigits++; - } - } - } - uint256 scale = 10 ** decimals; - uint256 fracScale = 10 ** (decimals - fracDigits); - return intPart * scale + fracPart * fracScale; - } -} diff --git a/solidity/archived/transfer/bank/libraries/LibTemplate.sol b/solidity/archived/transfer/bank/libraries/LibTemplate.sol deleted file mode 100644 index 7c30a0b2..00000000 --- a/solidity/archived/transfer/bank/libraries/LibTemplate.sol +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; - -import {LibString} from "./LibString.sol"; -import {LibTemplateStorage} from "../storage/LibTemplateStorage.sol"; -import { - TemplateNotFound, - NoTemplateMatch, - TemplateMissingToken, - TemplateMissingAmount, - TemplateMissingTokenAfterRecipient -} from "../BankErrors.sol"; - -/// @title LibTemplate — honor-command template parsing + matching. -/// @dev Parses a template string into `{prefix}{recipient}{afterRecipient}{amount} -/// {afterAmount}{token}{suffix}` parts and matches a revealed comment body -/// against them, extracting recipient/amount/token. Reads/writes -/// `LibTemplateStorage`; uses `LibString` for search/extraction (+ OZ -/// `Bytes.slice` for memory substrings, `Strings.toHexString` for by-address). -library LibTemplate { - /// @dev Store a template string and parse it into parts. Recognizes: - /// - `{recipient}`+`{amount}`+`{token}` — explicit recipient form. - /// - `{amount}`+`{token}` (no `{recipient}`) — recipient-less form - /// (recipient inferred off-chain from the reply/quote parent). - /// - no `{amount}` — a plain literal template (body must contain it). - function addTemplate(string memory platform, string memory template) internal { - LibTemplateStorage.TemplateStorage storage ts = LibTemplateStorage.store(); - bytes32 key = keccak256(bytes(platform)); - ts.platformTemplates[key].push(template); - - bytes memory t = bytes(template); - (bool foundRcp, uint256 rcpPos) = LibString.indexOf(t, bytes("{recipient}"), 0, false); - if (!foundRcp) { - (bool amtOnly, uint256 amtOnlyPos) = LibString.indexOf(t, bytes("{amount}"), 0, false); - if (!amtOnly) { - ts.templateParts[key].push( - LibTemplateStorage.TemplateParts({ - prefix: template, afterRecipient: "", afterAmount: "", suffix: "", hasPlaceholders: false - }) - ); - return; - } - (bool tokOnly, uint256 tokOnlyPos) = LibString.indexOf(t, bytes("{token}"), amtOnlyPos + 8, false); - if (!tokOnly) revert TemplateMissingToken(); - ts.templateParts[key].push( - LibTemplateStorage.TemplateParts({ - prefix: string(Bytes.slice(t, 0, amtOnlyPos)), - afterRecipient: "", - afterAmount: string(Bytes.slice(t, amtOnlyPos + 8, tokOnlyPos)), - suffix: string(Bytes.slice(t, tokOnlyPos + 7, t.length)), - hasPlaceholders: true - }) - ); - return; - } - (bool foundAmt, uint256 amtPos) = LibString.indexOf(t, bytes("{amount}"), rcpPos + 11, false); - if (!foundAmt) revert TemplateMissingAmount(); - (bool foundTok, uint256 tokPos) = LibString.indexOf(t, bytes("{token}"), amtPos + 8, false); - if (!foundTok) revert TemplateMissingTokenAfterRecipient(); - - ts.templateParts[key].push( - LibTemplateStorage.TemplateParts({ - prefix: string(Bytes.slice(t, 0, rcpPos)), - afterRecipient: string(Bytes.slice(t, rcpPos + 11, amtPos)), - afterAmount: string(Bytes.slice(t, amtPos + 8, tokPos)), - suffix: string(Bytes.slice(t, tokPos + 7, t.length)), - hasPlaceholders: true - }) - ); - } - - /// @dev Whether the i-th template for a platform is an explicit `{recipient}` - /// form. Derived at call time from the raw template string. - function templateHasRecipient(bytes32 platformKey, uint256 i) internal view returns (bool) { - LibTemplateStorage.TemplateStorage storage ts = LibTemplateStorage.store(); - (bool found,) = LibString.indexOf(bytes(ts.platformTemplates[platformKey][i]), bytes("{recipient}"), 0, false); - return found; - } - - /// @dev Try all templates for a platform; return the first match. - /// @return matched Whether any template matched. - /// @return amt Extracted amount (empty for a literal template). - /// @return tok Extracted token (empty for a literal template). - /// @return recipientLess True when the matched template is a placeholder form - /// with NO `{recipient}` — the recipient is then bound off-body via the - /// proven reply-target id leaf. - function matchAnyTemplate(bytes calldata data, bytes32 platformKey) - internal - view - returns (bool, string memory, string memory, bool) - { - LibTemplateStorage.TemplateStorage storage ts = LibTemplateStorage.store(); - uint256 count = ts.templateParts[platformKey].length; - for (uint256 i = 0; i < count; i++) { - (bool matched, string memory amt, string memory tok,) = matchTemplate(data, platformKey, i); - if (matched) { - LibTemplateStorage.TemplateParts storage parts = ts.templateParts[platformKey][i]; - bool recipientLess = parts.hasPlaceholders && !templateHasRecipient(platformKey, i); - return (true, amt, tok, recipientLess); - } - } - return (false, "", "", false); - } - - /// @dev Match one template (by index) against the revealed body. Returns - /// `(matched, extractedAmount, extractedToken, extractedRecipient)`. - /// Literal templates yield empty amount/token/recipient. - function matchTemplate(bytes calldata data, bytes32 platformKey, uint256 templateIndex) - internal - view - returns (bool, string memory, string memory, string memory) - { - LibTemplateStorage.TemplateParts storage p = - LibTemplateStorage.store().templateParts[platformKey][templateIndex]; - bytes memory prefix = bytes(p.prefix); - - // 1. Prefix (case-insensitive). - (bool foundPrefix, uint256 prefixPos) = LibString.indexOf(data, prefix, 0, true); - if (!foundPrefix) return (false, "", "", ""); - - // Simple literal template — a prefix match is sufficient. - if (!p.hasPlaceholders) return (true, "", "", ""); - - bytes memory afterRecipient = bytes(p.afterRecipient); - bytes memory afterAmount = bytes(p.afterAmount); - bytes memory suffix = bytes(p.suffix); - - uint256 recipientStart = prefixPos + prefix.length; - - // 2. afterRecipient (skip over the recipient value). - (bool foundAfterRcp, uint256 afterRcpPos) = LibString.indexOf(data, afterRecipient, recipientStart, true); - if (!foundAfterRcp) return (false, "", "", ""); - string memory recipient = LibString.substringCalldata(data, recipientStart, afterRcpPos); - uint256 amountStart = afterRcpPos + afterRecipient.length; - - // 3. afterAmount (skip over the amount value). - (bool foundAfterAmt, uint256 afterAmtPos) = LibString.indexOf(data, afterAmount, amountStart, true); - if (!foundAfterAmt) return (false, "", "", ""); - string memory amount = LibString.substringCalldata(data, amountStart, afterAmtPos); - uint256 tokenStart = afterAmtPos + afterAmount.length; - - // 4. suffix (or, when empty, the token runs to the next non-token char). - string memory token; - if (suffix.length > 0) { - (bool foundSuffix, uint256 suffixPos) = LibString.indexOf(data, suffix, tokenStart, true); - if (!foundSuffix) return (false, "", "", ""); - token = LibString.substringCalldata(data, tokenStart, suffixPos); - } else { - uint256 tokenEnd = tokenStart; - while (tokenEnd < data.length) { - if (!LibString.isTokenChar(data[tokenEnd])) break; - tokenEnd++; - } - token = LibString.substringCalldata(data, tokenStart, tokenEnd); - } - - if (bytes(amount).length == 0 || bytes(token).length == 0) return (false, "", "", ""); - return (true, amount, token, recipient); - } - - /// @dev Verify a template match and check the extracted amount + token - /// against `amountStr` / `expectedToken`. Reverts if nothing matches. - /// Returns `recipientLess` — true when a recipient-less template matched - /// (the caller binds the recipient via the proven reply-target id leaf). - /// The by-name vs by-address distinction is the caller's: it passes the - /// token NAME or `Strings.toHexString(tokenAddress)` as `expectedToken`. - function verifyTemplate( - bytes calldata body, - string calldata platform, - string calldata receiverHandle, - string calldata amountStr, - string memory expectedToken - ) internal view returns (bool recipientLess) { - bytes32 platformKey = keccak256(bytes(platform)); - uint256 count = LibTemplateStorage.store().platformTemplates[platformKey].length; - if (count == 0) revert TemplateNotFound(); - for (uint256 i = 0; i < count; i++) { - (bool matched, string memory amt, string memory tok, string memory rcp) = - matchTemplate(body, platformKey, i); - if ( - matched && keccak256(bytes(amt)) == keccak256(bytes(amountStr)) - && LibString.caseInsensitiveEqual(tok, expectedToken) - ) { - if (!templateHasRecipient(platformKey, i)) return true; - if (LibString.caseInsensitiveEqual(rcp, receiverHandle)) return false; - } - } - revert NoTemplateMatch(); - } -} diff --git a/solidity/archived/transfer/bank/storage/LibCoreStorage.sol b/solidity/archived/transfer/bank/storage/LibCoreStorage.sol deleted file mode 100644 index 1c14f037..00000000 --- a/solidity/archived/transfer/bank/storage/LibCoreStorage.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title LibCoreStorage — cross-cutting Bank config + lifecycle flags. -/// @dev Trusted-party pointers, replay set, and the pause / reentrancy flags -/// (replacing the OZ Pausable / ReentrancyGuard base contracts a diamond -/// cannot inherit). Ownership lives in `LibDiamond` (ERC-173). Its own -/// namespaced slot, isolated from the other Bank storage concerns. -library LibCoreStorage { - bytes32 internal constant STORAGE_POSITION = keccak256("dyaka.bank.core.v1"); - - struct CoreStorage { - address notary; // the shared Notary contract (INotary proxy) - address backend; // backend signer - address registry; // Registry (resolves WebWallet by id) - mapping(bytes32 => bool) usedTransfers; // replay protection by uid - bool paused; - bool initialized; // one-shot guard: BankInit.init sets it; a re-delegatecall reverts - uint256 reentrancyStatus; // 0/1 = not entered, 2 = entered - } - - function store() internal pure returns (CoreStorage storage cs) { - bytes32 position = STORAGE_POSITION; - assembly { - cs.slot := position - } - } -} diff --git a/solidity/archived/transfer/bank/storage/LibEscrowStorage.sol b/solidity/archived/transfer/bank/storage/LibEscrowStorage.sol deleted file mode 100644 index e2d4cb1c..00000000 --- a/solidity/archived/transfer/bank/storage/LibEscrowStorage.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title LibEscrowStorage — the Bank's token ledgers. -/// @dev The money: registered-wallet balances and id-escrow (unregistered) -/// balances, in their own namespaced slot — isolated from config/templates -/// so only the transfer/vault path touches funds. -library LibEscrowStorage { - bytes32 internal constant STORAGE_POSITION = keccak256("dyaka.bank.escrow.v1"); - - struct EscrowStorage { - // Registered users: wallet → token → amount. - mapping(address => mapping(address => uint256)) registeredBalances; - // Unregistered users (id-escrow): escrowKey → token → amount, where - // escrowKey = keccak256(abi.encode(ID_ESCROW_TAG, platform, userId)). - mapping(bytes32 => mapping(address => uint256)) unregisteredBalances; - } - - function store() internal pure returns (EscrowStorage storage es) { - bytes32 position = STORAGE_POSITION; - assembly { - es.slot := position - } - } -} diff --git a/solidity/archived/transfer/bank/storage/LibTemplateStorage.sol b/solidity/archived/transfer/bank/storage/LibTemplateStorage.sol deleted file mode 100644 index 5bff0068..00000000 --- a/solidity/archived/transfer/bank/storage/LibTemplateStorage.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title LibTemplateStorage — honor-template + resource/web-prefix config. -/// @dev Everything the template-matching path reads: per-platform template -/// strings, their parsed parts, and the resource-type / web-URL prefixes. -/// Its own namespaced slot — isolated from token registry and escrow. -library LibTemplateStorage { - bytes32 internal constant STORAGE_POSITION = keccak256("dyaka.bank.template.v1"); - - /// Parsed honor-template parts (parallel to `platformTemplates[key][i]`). - struct TemplateParts { - string prefix; // text before {recipient} (or full literal for simple templates) - string afterRecipient; // text between {recipient} and {amount} - string afterAmount; // text between {amount} and {token} - string suffix; // text after {token} - bool hasPlaceholders; // true when the template has {recipient}/{amount}/{token} - } - - struct TemplateStorage { - mapping(bytes32 => string[]) platformTemplates; // keccak(platform) → template strings - mapping(bytes32 => TemplateParts[]) templateParts; // parallel parsed parts - mapping(bytes32 => string) resourceTypePrefixes; // keccak(resourceType) → request-path prefix - mapping(bytes32 => string) platformWebPrefixes; // keccak(platform) → web URL prefix - } - - function store() internal pure returns (TemplateStorage storage ts) { - bytes32 position = STORAGE_POSITION; - assembly { - ts.slot := position - } - } -} diff --git a/solidity/archived/transfer/bank/storage/LibTokenStorage.sol b/solidity/archived/transfer/bank/storage/LibTokenStorage.sol deleted file mode 100644 index e08e535e..00000000 --- a/solidity/archived/transfer/bank/storage/LibTokenStorage.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title LibTokenStorage — the ERC-20 token registry. -/// @dev name↔address mapping (both directions) + the registered-token list, -/// in its own namespaced slot — isolated from templates and escrow. -library LibTokenStorage { - bytes32 internal constant STORAGE_POSITION = keccak256("dyaka.bank.token.v1"); - - struct TokenStorage { - mapping(bytes32 => address) tokenByName; // keccak(name) → ERC20 (case-sensitive) - mapping(address => string) tokenName; // ERC20 → canonical display name - address[] registeredTokens; - } - - function store() internal pure returns (TokenStorage storage ts) { - bytes32 position = STORAGE_POSITION; - assembly { - ts.slot := position - } - } -} diff --git a/solidity/archived/transfer/diamond/Diamond.sol b/solidity/archived/transfer/diamond/Diamond.sol deleted file mode 100644 index 812cc545..00000000 --- a/solidity/archived/transfer/diamond/Diamond.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibDiamond} from "./libraries/LibDiamond.sol"; -import {IDiamondCut} from "./interfaces/IDiamondCut.sol"; - -/// @title Diamond — EIP-2535 multi-facet proxy. -/// @dev The constructor sets the owner and cuts in the `DiamondCutFacet` so the -/// diamond can be extended. All other facets (loupe, ownership, Bank facets) -/// are added afterward via `diamondCut`. Every call is routed to a facet by -/// selector and executed via `delegatecall` in the diamond's storage context. -contract Diamond { - error FunctionNotFound(bytes4 selector); - - constructor(address _contractOwner, address _diamondCutFacet) payable { - LibDiamond.setContractOwner(_contractOwner); - - IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](1); - bytes4[] memory selectors = new bytes4[](1); - selectors[0] = IDiamondCut.diamondCut.selector; - cut[0] = IDiamondCut.FacetCut({ - facetAddress: _diamondCutFacet, action: IDiamondCut.FacetCutAction.Add, functionSelectors: selectors - }); - LibDiamond.diamondCut(cut, address(0), ""); - } - - fallback() external payable { - LibDiamond.DiamondStorage storage ds; - bytes32 position = LibDiamond.DIAMOND_STORAGE_POSITION; - assembly { - ds.slot := position - } - address facet = ds.selectorToFacetAndPosition[msg.sig].facetAddress; - if (facet == address(0)) revert FunctionNotFound(msg.sig); - assembly { - calldatacopy(0, 0, calldatasize()) - let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } - - receive() external payable {} -} diff --git a/solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol b/solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol deleted file mode 100644 index 2b68ba1b..00000000 --- a/solidity/archived/transfer/diamond/facets/DiamondCutFacet.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; - -/// @title DiamondCutFacet — add/replace/remove facet functions (owner-gated). -contract DiamondCutFacet is IDiamondCut { - /// @inheritdoc IDiamondCut - function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external override { - LibDiamond.enforceIsContractOwner(); - LibDiamond.diamondCut(_diamondCut, _init, _calldata); - } -} diff --git a/solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol b/solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol deleted file mode 100644 index eaf4fb7d..00000000 --- a/solidity/archived/transfer/diamond/facets/DiamondLoupeFacet.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {IDiamondLoupe} from "../interfaces/IDiamondLoupe.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -/// @title DiamondLoupeFacet — EIP-2535 introspection + ERC-165. -contract DiamondLoupeFacet is IDiamondLoupe, IERC165 { - /// @inheritdoc IDiamondLoupe - function facets() external view override returns (Facet[] memory facets_) { - LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage(); - uint256 n = ds.facetAddresses.length; - facets_ = new Facet[](n); - for (uint256 i; i < n; i++) { - address facet = ds.facetAddresses[i]; - facets_[i].facetAddress = facet; - facets_[i].functionSelectors = ds.facetFunctionSelectors[facet].functionSelectors; - } - } - - /// @inheritdoc IDiamondLoupe - function facetFunctionSelectors(address _facet) external view override returns (bytes4[] memory) { - return LibDiamond.diamondStorage().facetFunctionSelectors[_facet].functionSelectors; - } - - /// @inheritdoc IDiamondLoupe - function facetAddresses() external view override returns (address[] memory) { - return LibDiamond.diamondStorage().facetAddresses; - } - - /// @inheritdoc IDiamondLoupe - function facetAddress(bytes4 _functionSelector) external view override returns (address) { - return LibDiamond.diamondStorage().selectorToFacetAndPosition[_functionSelector].facetAddress; - } - - /// @inheritdoc IERC165 - function supportsInterface(bytes4 _interfaceId) external view override returns (bool) { - return LibDiamond.diamondStorage().supportedInterfaces[_interfaceId]; - } -} diff --git a/solidity/archived/transfer/diamond/facets/OwnershipFacet.sol b/solidity/archived/transfer/diamond/facets/OwnershipFacet.sol deleted file mode 100644 index da58073e..00000000 --- a/solidity/archived/transfer/diamond/facets/OwnershipFacet.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {LibDiamond} from "../libraries/LibDiamond.sol"; -import {IERC173} from "../interfaces/IERC173.sol"; - -/// @title OwnershipFacet — ERC-173 ownership backed by diamond storage. -contract OwnershipFacet is IERC173 { - /// @inheritdoc IERC173 - function transferOwnership(address _newOwner) external override { - LibDiamond.enforceIsContractOwner(); - LibDiamond.setContractOwner(_newOwner); - } - - /// @inheritdoc IERC173 - function owner() external view override returns (address owner_) { - owner_ = LibDiamond.contractOwner(); - } -} diff --git a/solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol b/solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol deleted file mode 100644 index 5099d2c5..00000000 --- a/solidity/archived/transfer/diamond/interfaces/IDiamondCut.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title EIP-2535 Diamond — cut interface. -/// @dev Add/replace/remove facet functions on a diamond. Reference: EIP-2535. -interface IDiamondCut { - enum FacetCutAction { - Add, - Replace, - Remove - } - - struct FacetCut { - address facetAddress; - FacetCutAction action; - bytes4[] functionSelectors; - } - - /// @notice Add/replace/remove any number of functions and optionally run a - /// delegatecall `_init` with `_calldata` in one transaction. - /// @param _diamondCut Facets and their function selectors to cut in. - /// @param _init Address of the contract to delegatecall (or `address(0)`). - /// @param _calldata Calldata for the `_init` delegatecall. - function diamondCut(FacetCut[] calldata _diamondCut, address _init, bytes calldata _calldata) external; - - event DiamondCut(FacetCut[] _diamondCut, address _init, bytes _calldata); -} diff --git a/solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol b/solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol deleted file mode 100644 index 0a41bed8..00000000 --- a/solidity/archived/transfer/diamond/interfaces/IDiamondLoupe.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title EIP-2535 Diamond — loupe (introspection) interface. -/// @dev Lets tools enumerate the facets and their selectors on a diamond. -interface IDiamondLoupe { - struct Facet { - address facetAddress; - bytes4[] functionSelectors; - } - - /// @notice All facet addresses and their function selectors. - function facets() external view returns (Facet[] memory facets_); - - /// @notice Function selectors provided by a facet. - function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory); - - /// @notice All facet addresses used by the diamond. - function facetAddresses() external view returns (address[] memory); - - /// @notice The facet that implements the given selector (or `address(0)`). - function facetAddress(bytes4 _functionSelector) external view returns (address); -} diff --git a/solidity/archived/transfer/diamond/interfaces/IERC173.sol b/solidity/archived/transfer/diamond/interfaces/IERC173.sol deleted file mode 100644 index 698f989a..00000000 --- a/solidity/archived/transfer/diamond/interfaces/IERC173.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @title ERC-173 — contract ownership standard (used by the diamond). -interface IERC173 { - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - - /// @notice The address of the owner. - function owner() external view returns (address owner_); - - /// @notice Transfer ownership. Set to `address(0)` to renounce. - function transferOwnership(address _newOwner) external; -} diff --git a/solidity/archived/transfer/diamond/libraries/LibDiamond.sol b/solidity/archived/transfer/diamond/libraries/LibDiamond.sol deleted file mode 100644 index d4434e59..00000000 --- a/solidity/archived/transfer/diamond/libraries/LibDiamond.sol +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IDiamondCut} from "../interfaces/IDiamondCut.sol"; - -/// @title LibDiamond — EIP-2535 diamond storage, cut, and ownership. -/// @dev Reference implementation (diamond-3, Nick Mudge), adapted. Holds the -/// diamond's OWN state (facet routing table + owner) in a fixed namespaced -/// slot, separate from any facet's application storage. -library LibDiamond { - bytes32 constant DIAMOND_STORAGE_POSITION = keccak256("diamond.standard.diamond.storage"); - - struct FacetAddressAndPosition { - address facetAddress; - uint96 functionSelectorPosition; // position in facetFunctionSelectors.functionSelectors - } - - struct FacetFunctionSelectors { - bytes4[] functionSelectors; - uint256 facetAddressPosition; // position of facetAddress in facetAddresses - } - - struct DiamondStorage { - mapping(bytes4 => FacetAddressAndPosition) selectorToFacetAndPosition; - mapping(address => FacetFunctionSelectors) facetFunctionSelectors; - address[] facetAddresses; - mapping(bytes4 => bool) supportedInterfaces; - address contractOwner; - } - - event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); - event DiamondCut(IDiamondCut.FacetCut[] _diamondCut, address _init, bytes _calldata); - - error NotContractOwner(address caller, address owner); - error NoSelectorsInFacet(); - error FacetAddressZeroWithSelectors(); - error FunctionAlreadyExists(bytes4 selector); - error FacetHasNoCode(address facet); - error CannotReplaceWithSameFunction(bytes4 selector); - error RemoveFacetMustBeZeroAddress(address facet); - error CannotRemoveNonexistentFunction(bytes4 selector); - error CannotRemoveImmutableFunction(bytes4 selector); - error IncorrectFacetCutAction(); - error InitAddressZeroWithCalldata(); - error InitCalldataEmptyWithAddress(); - error InitReverted(address init, bytes returndata); - - function diamondStorage() internal pure returns (DiamondStorage storage ds) { - bytes32 position = DIAMOND_STORAGE_POSITION; - assembly { - ds.slot := position - } - } - - function setContractOwner(address _newOwner) internal { - DiamondStorage storage ds = diamondStorage(); - address previousOwner = ds.contractOwner; - ds.contractOwner = _newOwner; - emit OwnershipTransferred(previousOwner, _newOwner); - } - - function contractOwner() internal view returns (address) { - return diamondStorage().contractOwner; - } - - function enforceIsContractOwner() internal view { - address owner_ = diamondStorage().contractOwner; - if (msg.sender != owner_) revert NotContractOwner(msg.sender, owner_); - } - - // ─── Diamond cut ──────────────────────────────────────────────────── - - function diamondCut(IDiamondCut.FacetCut[] memory _diamondCut, address _init, bytes memory _calldata) internal { - for (uint256 i; i < _diamondCut.length; i++) { - bytes4[] memory selectors = _diamondCut[i].functionSelectors; - address facet = _diamondCut[i].facetAddress; - if (selectors.length == 0) revert NoSelectorsInFacet(); - IDiamondCut.FacetCutAction action = _diamondCut[i].action; - if (action == IDiamondCut.FacetCutAction.Add) { - addFunctions(facet, selectors); - } else if (action == IDiamondCut.FacetCutAction.Replace) { - replaceFunctions(facet, selectors); - } else if (action == IDiamondCut.FacetCutAction.Remove) { - removeFunctions(facet, selectors); - } else { - revert IncorrectFacetCutAction(); - } - } - emit DiamondCut(_diamondCut, _init, _calldata); - initializeDiamondCut(_init, _calldata); - } - - function addFunctions(address _facet, bytes4[] memory _selectors) internal { - if (_facet == address(0)) revert FacetAddressZeroWithSelectors(); - DiamondStorage storage ds = diamondStorage(); - uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facet].functionSelectors.length); - if (selectorPosition == 0) addFacet(ds, _facet); - for (uint256 i; i < _selectors.length; i++) { - bytes4 selector = _selectors[i]; - if (ds.selectorToFacetAndPosition[selector].facetAddress != address(0)) { - revert FunctionAlreadyExists(selector); - } - addFunction(ds, selector, selectorPosition, _facet); - selectorPosition++; - } - } - - function replaceFunctions(address _facet, bytes4[] memory _selectors) internal { - if (_facet == address(0)) revert FacetAddressZeroWithSelectors(); - DiamondStorage storage ds = diamondStorage(); - uint96 selectorPosition = uint96(ds.facetFunctionSelectors[_facet].functionSelectors.length); - if (selectorPosition == 0) addFacet(ds, _facet); - for (uint256 i; i < _selectors.length; i++) { - bytes4 selector = _selectors[i]; - address old = ds.selectorToFacetAndPosition[selector].facetAddress; - if (old == _facet) revert CannotReplaceWithSameFunction(selector); - removeFunction(ds, old, selector); - addFunction(ds, selector, selectorPosition, _facet); - selectorPosition++; - } - } - - function removeFunctions(address _facet, bytes4[] memory _selectors) internal { - DiamondStorage storage ds = diamondStorage(); - // Remove facet address must be address(0). - if (_facet != address(0)) revert RemoveFacetMustBeZeroAddress(_facet); - for (uint256 i; i < _selectors.length; i++) { - bytes4 selector = _selectors[i]; - address old = ds.selectorToFacetAndPosition[selector].facetAddress; - removeFunction(ds, old, selector); - } - } - - function addFacet(DiamondStorage storage ds, address _facet) internal { - enforceHasContractCode(_facet); - ds.facetFunctionSelectors[_facet].facetAddressPosition = ds.facetAddresses.length; - ds.facetAddresses.push(_facet); - } - - function addFunction(DiamondStorage storage ds, bytes4 _selector, uint96 _position, address _facet) internal { - ds.selectorToFacetAndPosition[_selector].functionSelectorPosition = _position; - ds.facetFunctionSelectors[_facet].functionSelectors.push(_selector); - ds.selectorToFacetAndPosition[_selector].facetAddress = _facet; - } - - function removeFunction(DiamondStorage storage ds, address _facet, bytes4 _selector) internal { - if (_facet == address(0)) revert CannotRemoveNonexistentFunction(_selector); - // An immutable function is defined directly in the diamond (facet == diamond). - if (_facet == address(this)) revert CannotRemoveImmutableFunction(_selector); - - uint256 selectorPosition = ds.selectorToFacetAndPosition[_selector].functionSelectorPosition; - uint256 lastSelectorPosition = ds.facetFunctionSelectors[_facet].functionSelectors.length - 1; - if (selectorPosition != lastSelectorPosition) { - bytes4 lastSelector = ds.facetFunctionSelectors[_facet].functionSelectors[lastSelectorPosition]; - ds.facetFunctionSelectors[_facet].functionSelectors[selectorPosition] = lastSelector; - ds.selectorToFacetAndPosition[lastSelector].functionSelectorPosition = uint96(selectorPosition); - } - ds.facetFunctionSelectors[_facet].functionSelectors.pop(); - delete ds.selectorToFacetAndPosition[_selector]; - - if (lastSelectorPosition == 0) { - // Remove the facet address from facetAddresses. - uint256 lastFacetPosition = ds.facetAddresses.length - 1; - uint256 facetPosition = ds.facetFunctionSelectors[_facet].facetAddressPosition; - if (facetPosition != lastFacetPosition) { - address lastFacet = ds.facetAddresses[lastFacetPosition]; - ds.facetAddresses[facetPosition] = lastFacet; - ds.facetFunctionSelectors[lastFacet].facetAddressPosition = facetPosition; - } - ds.facetAddresses.pop(); - delete ds.facetFunctionSelectors[_facet].facetAddressPosition; - } - } - - function initializeDiamondCut(address _init, bytes memory _calldata) internal { - if (_init == address(0)) { - if (_calldata.length != 0) revert InitAddressZeroWithCalldata(); - return; - } - if (_calldata.length == 0) revert InitCalldataEmptyWithAddress(); - enforceHasContractCode(_init); - (bool success, bytes memory returndata) = _init.delegatecall(_calldata); - if (!success) { - if (returndata.length > 0) { - assembly { - revert(add(32, returndata), mload(returndata)) - } - } - revert InitReverted(_init, returndata); - } - } - - function enforceHasContractCode(address _contract) internal view { - if (_contract.code.length == 0) revert FacetHasNoCode(_contract); - } -} diff --git a/solidity/archived/transfer/script/BankDiamondDeployer.sol b/solidity/archived/transfer/script/BankDiamondDeployer.sol deleted file mode 100644 index 7defbe5a..00000000 --- a/solidity/archived/transfer/script/BankDiamondDeployer.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {CommonBase} from "forge-std/Base.sol"; - -import {Diamond} from "../diamond/Diamond.sol"; -import {DiamondCutFacet} from "../diamond/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "../diamond/facets/DiamondLoupeFacet.sol"; -import {OwnershipFacet} from "../diamond/facets/OwnershipFacet.sol"; -import {IDiamondCut} from "../diamond/interfaces/IDiamondCut.sol"; - -import {AdminFacet} from "../bank/facets/AdminFacet.sol"; -import {VaultFacet} from "../bank/facets/VaultFacet.sol"; -import {TransferFacet} from "../bank/facets/TransferFacet.sol"; -import {BankInit} from "../bank/BankInit.sol"; - -/// @title BankDiamondDeployer — assemble a fresh Bank diamond (facets + cut + init). -/// @dev Shared by the deploy script and the test suite so both wire the SAME facet -/// set. Facet selectors are read from the compiled `methodIdentifiers` (keys -/// are canonical signatures → `bytes4(keccak256(sig))`), so adding a function -/// to a facet needs no manual selector list here. -abstract contract BankDiamondDeployer is CommonBase { - /// @notice Deploy every facet, cut them into a new diamond, and run `BankInit`. - /// @param owner Diamond owner (ERC-173). - /// @param notary The shared Notary contract (verifies notary attestations). - /// @param backend Backend signer address. - /// @param registry Registry contract (identity resolution). - function deployBankDiamond(address owner, address notary, address backend, address registry) - internal - returns (address diamond) - { - DiamondCutFacet cutFacet = new DiamondCutFacet(); - diamond = address(new Diamond(owner, address(cutFacet))); - - DiamondLoupeFacet loupe = new DiamondLoupeFacet(); - OwnershipFacet ownership = new OwnershipFacet(); - AdminFacet admin = new AdminFacet(); - VaultFacet vault = new VaultFacet(); - TransferFacet transferFacet = new TransferFacet(); - BankInit initializer = new BankInit(); - - IDiamondCut.FacetCut[] memory cut = new IDiamondCut.FacetCut[](5); - cut[0] = _facetCut(address(loupe), "DiamondLoupeFacet"); - cut[1] = _facetCut(address(ownership), "OwnershipFacet"); - cut[2] = _facetCut(address(admin), "AdminFacet"); - cut[3] = _facetCut(address(vault), "VaultFacet"); - cut[4] = _facetCut(address(transferFacet), "TransferFacet"); - - IDiamondCut(diamond) - .diamondCut(cut, address(initializer), abi.encodeCall(BankInit.init, (notary, backend, registry))); - } - - function _facetCut(address facet, string memory name) private view returns (IDiamondCut.FacetCut memory) { - return IDiamondCut.FacetCut({ - facetAddress: facet, action: IDiamondCut.FacetCutAction.Add, functionSelectors: _selectors(name) - }); - } - - /// @dev All external selectors of a facet, from its artifact's methodIdentifiers. - function _selectors(string memory name) private view returns (bytes4[] memory sels) { - string memory json = vm.readFile(string.concat("out/", name, ".sol/", name, ".json")); - string[] memory sigs = vm.parseJsonKeys(json, ".methodIdentifiers"); - sels = new bytes4[](sigs.length); - for (uint256 i = 0; i < sigs.length; i++) { - sels[i] = bytes4(keccak256(bytes(sigs[i]))); - } - } -} diff --git a/solidity/archived/transfer/script/DeployBankDiamond.s.sol b/solidity/archived/transfer/script/DeployBankDiamond.s.sol deleted file mode 100644 index 41136f90..00000000 --- a/solidity/archived/transfer/script/DeployBankDiamond.s.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Script, console} from "forge-std/Script.sol"; -import {BankDiamondDeployer} from "./BankDiamondDeployer.sol"; - -/// @notice Deploy a fresh Bank diamond against an EXISTING Notary contract + -/// Registry (the migration seeds it afterwards). -/// -/// Usage: -/// forge script contracts/script/DeployBankDiamond.s.sol \ -/// --rpc-url $RPC_URL --broadcast --private-key $PRIVATE_KEY -/// -/// Env: -/// NOTARY_CONTRACT_ADDRESS — the shared Notary contract (Bank verifies notary attestations through it). -/// BACKEND_ADDRESS — backend signer address. -/// REGISTRY_CONTRACT_ADDRESS — Registry contract (identity resolution). -contract DeployBankDiamond is Script, BankDiamondDeployer { - function run() external { - string memory keyHex = vm.envOr("DEPLOYER_KEY", vm.envOr("PRIVATE_KEY", string(""))); - require(bytes(keyHex).length > 0, "set DEPLOYER_KEY or PRIVATE_KEY"); - if (bytes(keyHex).length == 64) keyHex = string.concat("0x", keyHex); - uint256 deployerKey = vm.parseUint(keyHex); - address deployer = vm.addr(deployerKey); - - address notaryContract = vm.envAddress("NOTARY_CONTRACT_ADDRESS"); - address backend = vm.envOr("BACKEND_ADDRESS", deployer); - address registry = vm.envAddress("REGISTRY_CONTRACT_ADDRESS"); - - vm.startBroadcast(deployerKey); - address diamond = deployBankDiamond(deployer, notaryContract, backend, registry); - vm.stopBroadcast(); - - console.log("=== Bank diamond deployed ==="); - console.log("Owner: ", deployer); - console.log("BANK_CONTRACT_ADDRESS= ", diamond); - } -} diff --git a/solidity/archived/transfer/test/BalanceViews.t.sol b/solidity/archived/transfer/test/BalanceViews.t.sol deleted file mode 100644 index 4ee93dd6..00000000 --- a/solidity/archived/transfer/test/BalanceViews.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IntegrationTest} from "./Integration.t.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; - -contract BalanceViewsTest is IntegrationTest { - // ── balanceOf(address, token) — bank balance only ────────────── - - function test_balanceOf_byAddress_registered() public { - WebWallet alice = _alice(); - _fund("x", "alice", 100e18); - - assertEq(bank.balanceOf(address(alice), address(token)), 100e18); - } - - function test_balanceOf_byAddress_withUnmigrated() public { - // Fund before registration → goes to unregisteredBalances - _fund("x", "alice", 50e18); - // Now register - WebWallet alice = _alice(); - // Fund again after registration → goes to registeredBalances - _fund("x", "alice", 30e18); - - // balanceOf should see both: 30 registered + 50 unmigrated - assertEq(bank.balanceOf(address(alice), address(token)), 80e18); - } - - function test_balanceOf_byAddress_unregisteredWallet() public { - // Unknown wallet address → 0 - assertEq(bank.balanceOf(address(0xdead), address(token)), 0); - } - - // ── balanceOf(platform, handle, token) — bank balance only ───── - - function test_balanceOf_byHandle_registered() public { - _alice(); - _fund("x", "alice", 200e18); - assertEq(bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)), 200e18); - } - - function test_balanceOf_byHandle_unregistered() public { - _fund("x", "nobody", 75e18); - assertEq(bank.balanceOf("api.x.com", "nobody", _idFor("nobody"), address(token)), 75e18); - } - - function test_balanceOf_byHandle_noBalance() public { - assertEq(bank.balanceOf("api.x.com", "ghost", _idFor("ghost"), address(token)), 0); - } - - // ── balanceOfTotal(address, token) — bank + wallet ERC20 ─────── - - function test_balanceOfTotal_byAddress() public { - WebWallet alice = _alice(); - _fund("x", "alice", 100e18); - _fundWallet(alice, 500e18); - // Total = 100 (bank) + 500 (wallet ERC20) - assertEq(bank.balanceOfTotal(address(alice), address(token)), 600e18); - } - - function test_balanceOfTotal_byAddress_onlyBank() public { - WebWallet alice = _alice(); - _fund("x", "alice", 100e18); - assertEq(bank.balanceOfTotal(address(alice), address(token)), 100e18); - } - - function test_balanceOfTotal_byAddress_onlyWallet() public { - WebWallet alice = _alice(); - _fundWallet(alice, 500e18); - assertEq(bank.balanceOfTotal(address(alice), address(token)), 500e18); - } - - function test_balanceOfTotal_byAddress_zero() public { - WebWallet alice = _alice(); - assertEq(bank.balanceOfTotal(address(alice), address(token)), 0); - } - - // ── balanceOfTotal(platform, handle, token) — bank + wallet ──── - - function test_balanceOfTotal_byHandle_registered() public { - WebWallet alice = _alice(); - _fund("x", "alice", 100e18); - _fundWallet(alice, 250e18); - // Total = 100 (bank) + 250 (wallet ERC20) - assertEq(bank.balanceOfTotal("api.x.com", "alice", _idFor("alice"), address(token)), 350e18); - } - - function test_balanceOfTotal_byHandle_unregistered() public { - // Unregistered: only bank balance, no wallet - _fund("x", "nobody", 75e18); - assertEq(bank.balanceOfTotal("api.x.com", "nobody", _idFor("nobody"), address(token)), 75e18); - } - - function test_balanceOfTotal_byHandle_noBalance() public { - assertEq(bank.balanceOfTotal("api.x.com", "ghost", _idFor("ghost"), address(token)), 0); - } -} diff --git a/solidity/archived/transfer/test/BankDiamond.t.sol b/solidity/archived/transfer/test/BankDiamond.t.sol deleted file mode 100644 index 10284f4c..00000000 --- a/solidity/archived/transfer/test/BankDiamond.t.sol +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; - -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {IDiamondCut} from "../diamond/interfaces/IDiamondCut.sol"; -import {IDiamondLoupe} from "../diamond/interfaces/IDiamondLoupe.sol"; -import {IERC173} from "../diamond/interfaces/IERC173.sol"; -import {AdminFacet} from "../bank/facets/AdminFacet.sol"; -import {BankInit} from "../bank/BankInit.sol"; -import {NotRegistered, AlreadyRegistered, AlreadyInitialized} from "../bank/BankErrors.sol"; - -/// @title BankDiamondTest — structural smoke test for the assembled Bank diamond. -/// @dev Verifies the deployer wires facets + runs BankInit, and that every facet -/// stays under the EIP-170 24576-byte limit. -contract BankDiamondTest is Test, BankDiamondDeployer { - uint256 constant EIP170_LIMIT = 24576; - - address diamond; - - function setUp() public { - // init only stores the pointers (no calls) → dummy non-zero addrs suffice. - diamond = deployBankDiamond(address(this), address(1), address(2), address(3)); - } - - /// One-shot: the deploy ran BankInit.init once; a second init via diamondCut - /// (re-delegatecall) must revert — else a later owner cut would silently - /// overwrite the trusted-party pointers and duplicate the honor templates. - function test_bankInit_reinit_reverts() public { - BankInit reinit = new BankInit(); - IDiamondCut.FacetCut[] memory noCut = new IDiamondCut.FacetCut[](0); - vm.expectRevert(AlreadyInitialized.selector); - IDiamondCut(diamond) - .diamondCut(noCut, address(reinit), abi.encodeCall(BankInit.init, (address(1), address(2), address(3)))); - } - - function test_ownerIsDeployer() public view { - assertEq(IERC173(diamond).owner(), address(this)); - } - - function test_loupeListsAllFacets() public view { - // 5 cut facets (loupe, ownership, admin, vault, transfer) + DiamondCutFacet. - assertEq(IDiamondLoupe(diamond).facetAddresses().length, 6); - } - - function test_supportsCoreInterfaces() public view { - assertTrue(IERC165(diamond).supportsInterface(type(IERC165).interfaceId)); - assertTrue(IERC165(diamond).supportsInterface(type(IDiamondCut).interfaceId)); - assertTrue(IERC165(diamond).supportsInterface(type(IDiamondLoupe).interfaceId)); - assertTrue(IERC165(diamond).supportsInterface(type(IERC173).interfaceId)); - } - - function test_initSeededTemplates() public view { - assertEq(AdminFacet(diamond).platformTemplateCount("api.x.com"), 4); - assertEq(AdminFacet(diamond).platformTemplateCount("api.github.com"), 2); - } - - function test_adminRoutingRegisterToken() public { - AdminFacet(diamond).registerToken("$DEV", address(0xBEEF)); - assertEq(AdminFacet(diamond).resolveToken("$DEV"), address(0xBEEF)); - } - - // ── Token-registry guards (review fixes) ────────────────────────────── - - /// A typo / never-registered name must revert — NOT silently wipe the native - /// (address(0)) registration. - function test_unregisterToken_typoDoesNotWipeNative() public { - AdminFacet(diamond).registerToken("TIA", address(0)); // native - AdminFacet(diamond).registerToken("USDC", address(0xBEEF)); - vm.expectRevert(NotRegistered.selector); - AdminFacet(diamond).unregisterToken("USDCC"); // typo - assertEq(AdminFacet(diamond).tokenName(address(0)), "TIA", "native was wiped"); - } - - /// The native asset is still unregisterable by its own name. - function test_unregisterNative_byName_ok() public { - AdminFacet(diamond).registerToken("TIA", address(0)); - AdminFacet(diamond).unregisterToken("TIA"); - assertEq(AdminFacet(diamond).tokenName(address(0)), ""); - } - - /// unregisterToken prunes the address from getRegisteredTokens (no stale entry). - function test_unregisterToken_prunesRegisteredList() public { - AdminFacet(diamond).registerToken("USDC", address(0xBEEF)); - AdminFacet(diamond).registerToken("DAI", address(0xCAFE)); - AdminFacet(diamond).unregisterToken("USDC"); - address[] memory toks = AdminFacet(diamond).getRegisteredTokens(); - for (uint256 i = 0; i < toks.length; i++) { - assertTrue(toks[i] != address(0xBEEF), "unregistered token still listed"); - } - } - - /// Registering the same address under a second name must revert (no dup entry). - function test_registerToken_duplicateAddressReverts() public { - AdminFacet(diamond).registerToken("USDC", address(0xBEEF)); - vm.expectRevert(AlreadyRegistered.selector); - AdminFacet(diamond).registerToken("USDC2", address(0xBEEF)); - } - - /// A name held by the NATIVE asset can't be hijacked for an ERC-20 (native - /// keeps address(0) in tokenByName, so the plain name check can't see it). - function test_registerToken_nativeNameHijack_reverts() public { - AdminFacet(diamond).registerToken("TIA", address(0)); // native under "TIA" - vm.expectRevert(AlreadyRegistered.selector); - AdminFacet(diamond).registerToken("TIA", address(0xBEEF)); // hijack attempt - // Native still owns the name; the registry stays consistent. - assertEq(AdminFacet(diamond).tokenName(address(0)), "TIA"); - assertEq(AdminFacet(diamond).resolveToken("TIA"), address(0)); - } - - /// Case-insensitive: webTransferV2 resolves the native asset case-insensitively, - /// so a case variant of the native name ("TIA" → "tia") can't be hijacked for an - /// ERC-20 either — else the registry desyncs and a by-name honor settles wrong. - function test_registerToken_nativeNameHijack_caseVariant_reverts() public { - AdminFacet(diamond).registerToken("TIA", address(0)); // native under "TIA" - vm.expectRevert(AlreadyRegistered.selector); - AdminFacet(diamond).registerToken("tia", address(0xBEEF)); // case-variant hijack - assertEq(AdminFacet(diamond).tokenName(address(0)), "TIA"); - } - - /// @notice Every deployed facet must fit the EIP-170 contract-size limit. - function test_facetsUnderEip170() public view { - address[] memory facets = IDiamondLoupe(diamond).facetAddresses(); - for (uint256 i = 0; i < facets.length; i++) { - assertLe(facets[i].code.length, EIP170_LIMIT, "facet exceeds EIP-170 24KB"); - } - } -} diff --git a/solidity/archived/transfer/test/BankDiamondHonor.t.sol b/solidity/archived/transfer/test/BankDiamondHonor.t.sol deleted file mode 100644 index 663f2de0..00000000 --- a/solidity/archived/transfer/test/BankDiamondHonor.t.sol +++ /dev/null @@ -1,273 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; - -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "../bank/BankTypes.sol"; -import {TransferFacet} from "../bank/facets/TransferFacet.sol"; -import {VaultFacet} from "../bank/facets/VaultFacet.sol"; -import {AdminFacet} from "../bank/facets/AdminFacet.sol"; -import {IRegistry} from "../../login/IRegistry.sol"; -import {MockERC20} from "../MockERC20.sol"; -import {InvalidMerkleProof, NoTemplateMatch} from "../bank/BankErrors.sol"; - -/// @dev Minimal Notary — accepts an EIP-191 signature from the one signer. -contract MockNotary { - address public immutable notary; - - constructor(address signer_) { - notary = signer_; - } - - function verify(bytes32 digest, bytes calldata proof) external view returns (bool) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - return ECDSA.recover(ethHash, proof) == notary; - } -} - -/// @dev Minimal Registry — id→wallet resolution set directly (no proof harness). -contract MockRegistry is IRegistry { - mapping(bytes32 => address) internal _byId; - mapping(address => string[]) internal _platforms; - mapping(address => string[]) internal _ids; - - function setId(string calldata platform, string calldata id, address wallet) external { - _byId[keccak256(abi.encode(platform, id))] = wallet; - _platforms[wallet].push(platform); - _ids[wallet].push(id); - } - - function resolveById(string calldata platform, string calldata id) external view returns (address) { - return _byId[keccak256(abi.encode(platform, id))]; - } - - function getUserIds(address wallet) external view returns (string[] memory, string[] memory) { - return (_platforms[wallet], _ids[wallet]); - } - - function getHandles(address wallet) external view returns (string[] memory, string[] memory) { - return (_platforms[wallet], _ids[wallet]); - } - - function resolve(string calldata, string calldata) external pure returns (address) { - return address(0); - } - - function handleHint(string calldata, string calldata) external pure returns (string memory) { - return ""; - } -} - -/// @title BankDiamondHonorTest — end-to-end honor flow on the assembled diamond. -/// @dev Validates the ported TransferFacet orchestration (LibProof + LibTemplate + -/// LibEscrow) with real notary/backend signatures + Merkle proofs, using mock -/// Registry/Notary so the identity layer needs no proof harness. Proof -/// math mirrors Integration.t.sol's `_buildBankProofWithId`. -contract BankDiamondHonorTest is Test, BankDiamondDeployer { - uint256 constant NOTARY_KEY = 0xA11CE; - uint256 constant BACKEND_KEY = 0xB0B; - address constant ALICE_WALLET = address(0xA1); - address constant BOB_WALLET = address(0xB2); - - address diamond; - MockRegistry registry; - MockERC20 token; - - function setUp() public { - MockNotary notaryReg = new MockNotary(vm.addr(NOTARY_KEY)); - registry = new MockRegistry(); - diamond = deployBankDiamond(address(this), address(notaryReg), vm.addr(BACKEND_KEY), address(registry)); - - token = new MockERC20("Test", "TST"); - AdminFacet(diamond).registerToken("TST", address(token)); - - // alice + bob are registered (id → wallet); fund alice via deposit. - registry.setId("api.github.com", _idFor("alice"), ALICE_WALLET); - registry.setId("api.github.com", _idFor("bob"), BOB_WALLET); - - token.mint(address(this), 1000e18); - token.approve(diamond, 1000e18); - VaultFacet(diamond).deposit("api.github.com", "alice", _idFor("alice"), address(token), 1000e18); - } - - function test_honorHappyPath_movesBalances() public { - ( - ResourceInfo memory resource, - SenderInfo memory sender, - bytes memory body, - NotaryTlsProof memory proof, - BackendSig memory bsig - ) = _honor( - "repo/x/comment/1", "/repos/o/r/issues/comments/1", "@dyaka-agent honor @bob with 400 TST", "alice", "bob" - ); - - uint256 aliceBefore = VaultFacet(diamond).balanceOf("api.github.com", "alice", _idFor("alice"), address(token)); - TransferFacet(diamond) - .webTransferV2(resource, sender, body, proof, bsig, "bob", _idFor("bob"), "TST", "400", 400e18, ""); - - assertEq( - VaultFacet(diamond).balanceOf("api.github.com", "alice", _idFor("alice"), address(token)), - aliceBefore - 400e18 - ); - assertEq(VaultFacet(diamond).balanceOf("api.github.com", "bob", _idFor("bob"), address(token)), 400e18); - } - - /// Forged author-id (victim id) with no proven author-id leaf → recv-leaf rejects. - function test_honorForgedAuthorId_reverts() public { - ( - ResourceInfo memory resource, - SenderInfo memory sender, - bytes memory body, - NotaryTlsProof memory proof, - BackendSig memory bsig - ) = _honor( - "repo/x/comment/2", "/repos/o/r/issues/comments/2", "@dyaka-agent honor @bob with 400 TST", "alice", "bob" - ); - - sender.revealedAuthorId = abi.encodePacked('"author_id":"', _idFor("bob"), '"'); // forged, not in proof - vm.expectRevert(InvalidMerkleProof.selector); - TransferFacet(diamond) - .webTransferV2(resource, sender, body, proof, bsig, "bob", _idFor("bob"), "TST", "400", 400e18, ""); - } - - /// Body honors bob but caller swaps receiverHandle → recipient bind rejects. - function test_honorRecipientSwap_reverts() public { - ( - ResourceInfo memory resource, - SenderInfo memory sender, - bytes memory body, - NotaryTlsProof memory proof, - BackendSig memory bsig - ) = _honor( - "repo/x/comment/3", "/repos/o/r/issues/comments/3", "@dyaka-agent honor @bob with 400 TST", "alice", "bob" - ); - - vm.expectRevert(NoTemplateMatch.selector); - TransferFacet(diamond) - .webTransferV2( - resource, sender, body, proof, bsig, "attacker", _idFor("attacker"), "TST", "400", 400e18, "" - ); - } - - // ── proof helpers (mirror Integration.t.sol) ───────────────────────── - - function _honor( - string memory resourceId, - string memory requestPath, - string memory bodyStr, - string memory senderHandle, - string memory receiverHandle - ) - internal - view - returns ( - ResourceInfo memory resource, - SenderInfo memory sender, - bytes memory body, - NotaryTlsProof memory proof, - BackendSig memory bsig - ) - { - body = bytes(bodyStr); - bytes memory authorBytes = bytes(senderHandle); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor(senderHandle), '"'); - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - - proof = _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - bsig = _buildBackendSig(uid, body, authorBytes, authorId, _idFor(receiverHandle), block.timestamp); - resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - sender = SenderInfo({author: senderHandle, revealedAuthor: authorBytes, revealedAuthorId: authorId}); - } - - function _idFor(string memory handle) internal pure returns (string memory) { - return string(abi.encodePacked("id-", handle)); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _hp(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - function _buildBankProofWithId( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 domainHash = keccak256(bytes(apiHost)); - bytes memory serverEphKey = hex"cafebabe"; - - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - bytes32 leafId = _leaf("recv:", revealedAuthorId); - - bytes32 hDE = _hp(leafD, leafE); - bytes32 hBA = _hp(leafB, leafA); - bytes32 root4 = _hp(hDE, hBA); - bytes32 transcriptRoot = _hp(root4, leafId); - - proof.domainHash = domainHash; - proof.clientRandom = bytes32(uint256(333)); - proof.serverRandom = bytes32(uint256(444)); - proof.serverEphemeralKey = serverEphKey; - proof.transcriptRoot = transcriptRoot; - proof.timestamp = ts; - - proof.bodyMerklePath = _path3(leafA, hDE, leafId); - proof.authorMerklePath = _path3(leafB, hDE, leafId); - proof.domainMerklePath = _path3(leafE, hBA, leafId); - proof.endpointMerklePath = _path3(leafD, hBA, leafId); - proof.authorIdMerklePath = new bytes32[](1); - proof.authorIdMerklePath[0] = root4; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - proof.clientRandom, - proof.serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - proof.notarySig = _sign(NOTARY_KEY, notaryDigest); - } - - function _buildBackendSig( - bytes32 uid, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - string memory receiverUserId, - uint256 ts - ) internal pure returns (BackendSig memory) { - bytes32 digest = keccak256( - abi.encodePacked(uid, revealedBody, revealedAuthor, revealedAuthorId, keccak256(bytes(receiverUserId)), ts) - ); - return BackendSig({sig: _sign(BACKEND_KEY, digest), timestamp: ts}); - } - - function _path3(bytes32 a, bytes32 b, bytes32 c) internal pure returns (bytes32[] memory p) { - p = new bytes32[](3); - (p[0], p[1], p[2]) = (a, b, c); - } -} diff --git a/solidity/archived/transfer/test/DollarToken.t.sol b/solidity/archived/transfer/test/DollarToken.t.sol deleted file mode 100644 index 8817425b..00000000 --- a/solidity/archived/transfer/test/DollarToken.t.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; - -contract DollarTokenTest is Test, BankDiamondDeployer { - IBank bank; - - function setUp() public { - Notary notaryReg = deployNotary(address(this), address(1)); - bank = IBank(deployBankDiamond(address(this), address(notaryReg), address(2), address(3))); - } - - function test_registerDollarTIA() public { - address mockToken = address(0xBEEF); - - // Register $TIA as a token name — should not revert - bank.registerToken("$TIA", mockToken); - - // Verify it resolves - address resolved = bank.resolveToken("$TIA"); - assertEq(resolved, mockToken, "resolveToken should return the registered address"); - } - - function test_templateRenderWithDollarToken() public { - address mockToken = address(0xBEEF); - bank.registerToken("$TIA", mockToken); - - // Set a custom template on a fresh platform - bank.setPlatformTemplate("custom", "@dyaka-agent honor @{recipient} with {amount} {token}"); - - // Verify template parts work - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("custom", 0); - assertEq(prefix, "@dyaka-agent honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " "); - assertEq(suffix, ""); - } -} diff --git a/solidity/archived/transfer/test/EdgeCases.t.sol b/solidity/archived/transfer/test/EdgeCases.t.sol deleted file mode 100644 index 90e19195..00000000 --- a/solidity/archived/transfer/test/EdgeCases.t.sol +++ /dev/null @@ -1,590 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -/// @notice Edge-case and boundary tests for the refactored contracts. - -import {Test} from "forge-std/Test.sol"; -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -import {WebWallet} from "../../login/WebWallet.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import { - ZeroAmount, - InsufficientBalance, - TokenNotRegistered, - ZeroAddress, - AlreadyRegistered, - EmptyName, - NotRegistered, - NativeSentWithErc20, - InsufficientAllowance -} from "../bank/BankErrors.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; - -// ─── helpers ──────────────────────────────────────────────────────────────── - -contract MockToken is ERC20 { - constructor(string memory sym) ERC20(sym, sym) {} - - function mint(address to, uint256 amt) external { - _mint(to, amt); - } -} - -/// @dev Mock Registry for edge-case tests. Supports resolve and getHandles. -contract MockRegistryForEdge { - struct HandleEntry { - string platform; - string handle; - } - - mapping(string => mapping(string => address)) private _resolve; - mapping(string => mapping(string => string)) private _hint; - mapping(address => HandleEntry[]) private _handles; - - function setResolve(string calldata platform, string calldata handle, address wallet) external { - _resolve[platform][handle] = wallet; - _handles[wallet].push(HandleEntry(platform, handle)); - } - - function setHint(string calldata platform, string calldata handle, string calldata id) external { - _hint[platform][handle] = id; - } - - function resolve(string calldata platform, string calldata handle) external view returns (address) { - return _resolve[platform][handle]; - } - - // This mock keys identity by userId == handle, so resolveById mirrors resolve. - function resolveById(string calldata platform, string calldata id) external view returns (address) { - return _resolve[platform][id]; - } - - // Unbound by default; setHint simulates a handle pointing to a specific id. - function handleHint(string calldata platform, string calldata handle) external view returns (string memory) { - return _hint[platform][handle]; - } - - function getHandles(address wallet) external view returns (string[] memory platforms, string[] memory handles) { - HandleEntry[] storage entries = _handles[wallet]; - uint256 len = entries.length; - platforms = new string[](len); - handles = new string[](len); - for (uint256 i = 0; i < len; i++) { - platforms[i] = entries[i].platform; - handles[i] = entries[i].handle; - } - } - - // This mock keys deposits/transfers by userId == handle, so report the - // handle as the immutable userId — lets _migrate sweep the id-escrow slot. - function getUserIds(address wallet) external view returns (string[] memory platforms, string[] memory userIds) { - HandleEntry[] storage entries = _handles[wallet]; - uint256 len = entries.length; - platforms = new string[](len); - userIds = new string[](len); - for (uint256 i = 0; i < len; i++) { - platforms[i] = entries[i].platform; - userIds[i] = entries[i].handle; - } - } - - // This mock keys identity by userId == handle, so pass the handle as userId. - function registerSession(WebWallet wallet, string calldata platform, string calldata handle, address sessionAddr) - external - { - wallet.register_session(platform, handle, handle, sessionAddr); - } - - fallback() external payable {} - receive() external payable {} -} - -// ─── base setup ───────────────────────────────────────────────────────────── - -abstract contract EdgeBase is Test, BankDiamondDeployer { - address govOwner = makeAddr("govOwner"); - address depositor = makeAddr("depositor"); - MockRegistryForEdge mockRegistryContract; - address mockRegistry; - - uint256 notaryPk = 0xBE01; - uint256 backendPk = 0xBE02; - address notaryAddr; - address backendAddr; - - uint256 alicePk = 0xBE10; - address aliceSessionAddr; - - uint256 bobPk = 0xBE20; - address bobSessionAddr; - - function setUp() public virtual { - notaryAddr = vm.addr(notaryPk); - backendAddr = vm.addr(backendPk); - - aliceSessionAddr = vm.addr(alicePk); - bobSessionAddr = vm.addr(bobPk); - - mockRegistryContract = new MockRegistryForEdge(); - mockRegistry = address(mockRegistryContract); - - vm.warp(1_000_000); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - /// @dev Deploy a WebWallet with a session registered via mock registry. - function _deployWalletWithSession(string memory platform, string memory handle, uint256 sessionPk_) - internal - returns (WebWallet wallet) - { - address sAddr = vm.addr(sessionPk_); - wallet = new WebWallet(); - wallet.initialize(mockRegistry, address(0), platform, handle); - mockRegistryContract.registerSession(wallet, platform, handle, sAddr); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 1. WebWallet edge cases -// ═══════════════════════════════════════════════════════════════════════════ - -contract WebWalletEdgeCases is EdgeBase { - WebWallet wallet; - - function setUp() public override { - super.setUp(); - wallet = _deployWalletWithSession("tiktok", "alice", alicePk); - } - - function test_registerSession_revertsIfNotRegistry() public { - vm.expectRevert(WebWallet.OnlyRegistry.selector); - wallet.register_session("tiktok", "alice", "alice", aliceSessionAddr); - } - - function test_execute_incrementsNonce() public { - assertEq(wallet.nonce(), 0); - - bytes memory data = hex""; - bytes32 digest = keccak256( - abi.encode(mockRegistry, uint256(0), keccak256(data), uint256(0), block.chainid, address(wallet)) - ); - - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(alicePk, digest); - - wallet.execute(mockRegistry, 0, data, sigs); - assertEq(wallet.nonce(), 1); - } - - function test_execute_samePairCountsOnce() public { - mockRegistryContract.registerSession(wallet, "tiktok", "alice", bobSessionAddr); - - bytes memory data = hex""; - bytes32 digest = keccak256( - abi.encode(mockRegistry, uint256(0), keccak256(data), uint256(0), block.chainid, address(wallet)) - ); - - bytes[] memory sigs = new bytes[](2); - sigs[0] = _sign(alicePk, digest); - sigs[1] = _sign(bobPk, digest); - - wallet.execute(mockRegistry, 0, data, sigs); - assertEq(wallet.nonce(), 1); - } -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 2. Bank edge cases -// ═══════════════════════════════════════════════════════════════════════════ - -contract BankEdgeCases is EdgeBase { - // Mirrors AdminFacet's RegistrySet — for expectEmit (no registry() getter on the diamond). - event RegistrySet(address indexed oldRegistry, address indexed newRegistry); - - IBank bank; - MockToken usdc; - MockToken dai; - WebWallet aliceWallet; - - function setUp() public override { - super.setUp(); - - usdc = new MockToken("USDC"); - dai = new MockToken("DAI"); - - aliceWallet = _deployWalletWithSession("tiktok", "alice", alicePk); - mockRegistryContract.setResolve("tiktok", "alice", address(aliceWallet)); - - { - Notary notaryReg = deployNotary(address(this), notaryAddr); - bank = IBank(deployBankDiamond(address(this), address(notaryReg), backendAddr, mockRegistry)); - } - bank.registerToken("USDC", address(usdc)); - bank.registerToken("DAI", address(dai)); - - usdc.mint(depositor, 1_000e18); - dai.mint(depositor, 1_000e18); - vm.startPrank(depositor); - usdc.approve(address(bank), type(uint256).max); - dai.approve(address(bank), type(uint256).max); - vm.stopPrank(); - } - - function _deposit(address tkn, uint256 amt) internal { - vm.prank(depositor); - bank.deposit("tiktok", "alice", "alice", tkn, amt); - } - - function _executeWithdraw(address tkn, uint256 amt) internal { - bytes memory withdrawData = abi.encodeCall(IBank.withdraw, (address(aliceWallet), tkn, amt)); - - uint256 currentNonce = aliceWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(bank), uint256(0), keccak256(withdrawData), currentNonce, block.chainid, address(aliceWallet) - ) - ); - - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(alicePk, digest); - - aliceWallet.execute(address(bank), 0, withdrawData, sigs); - } - - // ── Deposit routing ──────────────────────────────────────────── - - function test_deposit_registeredGoesToRegisteredBalances() public { - _deposit(address(usdc), 100e18); - // Alice is registered, so should be in registeredBalances - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 100e18); - } - - function test_deposit_unregisteredGoesToUnregisteredBalances() public { - vm.prank(depositor); - bank.deposit("tiktok", "bob", "bob", address(usdc), 100e18); - // Bob is not registered — id-escrow is domain-separated by "id:v1" - bytes32 key = keccak256(abi.encode("id:v1", "tiktok", "bob")); - assertEq(bank.unregisteredBalances(key, address(usdc)), 100e18); - } - - // Recycled handle (hint id != provided id): escrow under the id, don't credit prior owner. - function test_deposit_recycledHandle_escrowsUnderIdNotPriorOwner() public { - mockRegistryContract.setHint("tiktok", "alice", "111"); - vm.prank(depositor); - bank.deposit("tiktok", "alice", "999", address(usdc), 100e18); - - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 0); - bytes32 key = keccak256(abi.encode("id:v1", "tiktok", "999")); - assertEq(bank.unregisteredBalances(key, address(usdc)), 100e18); - } - - // Matching hint (same id) still resolves the registered wallet. - function test_deposit_matchingHint_creditsRegisteredWallet() public { - mockRegistryContract.setHint("tiktok", "alice", "alice"); - vm.prank(depositor); - bank.deposit("tiktok", "alice", "alice", address(usdc), 100e18); - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 100e18); - } - - function test_deposit_revertsOnZeroAmount() public { - vm.prank(depositor); - vm.expectRevert(ZeroAmount.selector); - bank.deposit("tiktok", "alice", "alice", address(usdc), 0); - } - - // ── Withdraw ──────────────────────────────────────────────────── - - function test_withdraw_success() public { - _deposit(address(usdc), 100e18); - _executeWithdraw(address(usdc), 50e18); - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 50e18); - assertEq(usdc.balanceOf(address(aliceWallet)), 50e18); - } - - function test_withdraw_revertsIfNotRegisteredWallet() public { - _deposit(address(usdc), 100e18); - address anyone = makeAddr("anyone"); - vm.prank(anyone); - vm.expectRevert(InsufficientBalance.selector); - bank.withdraw(anyone, address(usdc), 100e18); - } - - function test_withdraw_revertsOnOverdraft() public { - _deposit(address(usdc), 50e18); - - bytes memory withdrawData = abi.encodeCall(IBank.withdraw, (address(aliceWallet), address(usdc), 100e18)); - uint256 currentNonce = aliceWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(bank), uint256(0), keccak256(withdrawData), currentNonce, block.chainid, address(aliceWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(alicePk, digest); - - vm.expectRevert(); // InsufficientBalance wrapped in ExecutionFailed - aliceWallet.execute(address(bank), 0, withdrawData, sigs); - } - - // ── Lazy migration ────────────────────────────────────────────── - - function test_withdraw_migratesUnregisteredFirst() public { - // Deposit as unregistered first (before alice is "known" for this key) - // We'll simulate: put tokens in unregisteredBalances for alice's handle - // by depositing for "tiktok"/"alice" when she's NOT resolved yet - - // Create a new bank where alice is not yet resolved - MockRegistryForEdge freshRegistry = new MockRegistryForEdge(); - IBank freshBank; - { - Notary notaryReg2 = deployNotary(address(this), notaryAddr); - freshBank = - IBank(deployBankDiamond(address(this), address(notaryReg2), backendAddr, address(freshRegistry))); - } - freshBank.registerToken("USDC", address(usdc)); - - // Deposit while alice is unregistered - vm.prank(depositor); - usdc.approve(address(freshBank), type(uint256).max); - vm.prank(depositor); - freshBank.deposit("tiktok", "alice", "alice", address(usdc), 100e18); - - bytes32 key = keccak256(abi.encode("id:v1", "tiktok", "alice")); - assertEq(freshBank.unregisteredBalances(key, address(usdc)), 100e18); - - // Now register alice - freshRegistry.setResolve("tiktok", "alice", address(aliceWallet)); - - // Withdraw should migrate first - bytes memory withdrawData = abi.encodeCall(IBank.withdraw, (address(aliceWallet), address(usdc), 60e18)); - uint256 currentNonce = aliceWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(freshBank), - uint256(0), - keccak256(withdrawData), - currentNonce, - block.chainid, - address(aliceWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(alicePk, digest); - aliceWallet.execute(address(freshBank), 0, withdrawData, sigs); - - // Unregistered should be zero, registered should have remainder - assertEq(freshBank.unregisteredBalances(key, address(usdc)), 0); - assertEq(freshBank.registeredBalances(address(aliceWallet), address(usdc)), 40e18); - } - - // ── balanceOf ─────────────────────────────────────────────────── - - function test_balanceOf_handleVersion_registered() public { - _deposit(address(usdc), 100e18); - assertEq(bank.balanceOf("tiktok", "alice", "alice", address(usdc)), 100e18); - } - - function test_balanceOf_handleVersion_unregistered() public { - vm.prank(depositor); - bank.deposit("tiktok", "bob", "bob", address(usdc), 100e18); - assertEq(bank.balanceOf("tiktok", "bob", "bob", address(usdc)), 100e18); - } - - function test_balanceOf_walletVersion() public { - _deposit(address(usdc), 100e18); - assertEq(bank.balanceOf(address(aliceWallet), address(usdc)), 100e18); - } - - function test_balanceOf_returnsZeroForUnknownIdentity() public view { - assertEq(bank.balanceOf("discord", "nobody", "nobody", address(usdc)), 0); - } - - // ── Multiple tokens ───────────────────────────────────────────── - - // ── deposit with unregistered token ──────────────────────────── - - function test_deposit_revertsWithUnregisteredToken() public { - MockToken rando = new MockToken("RANDO"); - rando.mint(depositor, 100e18); - vm.prank(depositor); - rando.approve(address(bank), 100e18); - vm.prank(depositor); - vm.expectRevert(TokenNotRegistered.selector); - bank.deposit("tiktok", "alice", "alice", address(rando), 100e18); - } - - // ── withdraw zero amount ──────────────────────────────────────── - - function test_withdraw_revertsOnZeroAmount() public { - _deposit(address(usdc), 100e18); - bytes memory withdrawData = abi.encodeCall(IBank.withdraw, (address(aliceWallet), address(usdc), 0)); - uint256 currentNonce = aliceWallet.nonce(); - bytes32 digest = keccak256( - abi.encode( - address(bank), uint256(0), keccak256(withdrawData), currentNonce, block.chainid, address(aliceWallet) - ) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(alicePk, digest); - vm.expectRevert(); // ZeroAmount wrapped in ExecutionFailed - aliceWallet.execute(address(bank), 0, withdrawData, sigs); - } - - // ── setRegistry ───────────────────────────────────────────────── - - function test_setRegistry_onlyOwner() public { - address newRegistry = makeAddr("newReg"); - // Non-owner fails - vm.prank(depositor); - vm.expectRevert(); - bank.setRegistry(newRegistry); - } - - function test_setRegistry_updatesRegistry() public { - address newRegistry = makeAddr("newReg"); - vm.expectEmit(true, true, false, true, address(bank)); - emit RegistrySet(mockRegistry, newRegistry); - bank.setRegistry(newRegistry); - assertEq(bank.registry(), newRegistry); - } - - function test_setRegistry_revertsZeroAddress() public { - vm.expectRevert(ZeroAddress.selector); - bank.setRegistry(address(0)); - } - - // ── registerToken / unregisterToken ────────────────────────────── - - function test_registerToken_revertsIfNotOwner() public { - vm.prank(depositor); - vm.expectRevert(); - bank.registerToken("X", address(1)); - } - - function test_registerToken_revertsIfAlreadyRegistered() public { - vm.expectRevert(AlreadyRegistered.selector); - bank.registerToken("USDC", address(usdc)); - } - - function test_registerToken_nativeAssetAllowed() public { - // address(0) is now allowed as native asset. - bank.registerToken("ETH", address(0)); - assertEq(bank.tokenName(address(0)), "ETH"); - } - - function test_registerToken_revertsEmptyName() public { - vm.expectRevert(EmptyName.selector); - bank.registerToken("", address(1)); - } - - function test_unregisterToken_success() public { - bank.unregisterToken("USDC"); - assertEq(bank.resolveToken("USDC"), address(0)); - assertEq(bank.tokenName(address(usdc)), ""); - } - - function test_unregisterToken_revertsIfNotRegistered() public { - vm.expectRevert(NotRegistered.selector); - bank.unregisterToken("NONEXISTENT"); - } - - function test_unregisterToken_revertsIfNotOwner() public { - vm.prank(depositor); - vm.expectRevert(); - bank.unregisterToken("USDC"); - } - - function test_deposit_revertsAfterUnregister() public { - bank.unregisterToken("USDC"); - vm.prank(depositor); - vm.expectRevert(TokenNotRegistered.selector); - bank.deposit("tiktok", "alice", "alice", address(usdc), 100e18); - } - - // ── resolveToken ──────────────────────────────────────────────── - - function test_resolveToken_returnsCorrectAddress() public view { - assertEq(bank.resolveToken("USDC"), address(usdc)); - } - - function test_resolveToken_returnsZeroForUnknown() public view { - assertEq(bank.resolveToken("NOPE"), address(0)); - } - - // ── balanceOf with multiple linked handles ────────────────────── - - function test_balanceOf_aggregatesAcrossHandles() public { - // Setup bob wallet with second handle - WebWallet bobWallet = _deployWalletWithSession("tiktok", "bob", bobPk); - mockRegistryContract.setResolve("tiktok", "bob", address(bobWallet)); - - // Add a second handle for bob - mockRegistryContract.setResolve("github", "bob_gh", address(bobWallet)); - mockRegistryContract.registerSession(bobWallet, "github", "bob_gh", makeAddr("bobGhSession")); - - // Deposit to both handles - vm.prank(depositor); - bank.deposit("tiktok", "bob", "bob", address(usdc), 100e18); - - // Deposit to unregistered second handle directly - // Since mockRegistry already maps github/bob_gh -> bobWallet, deposit goes to registered - vm.prank(depositor); - bank.deposit("github", "bob_gh", "bob_gh", address(usdc), 200e18); - - // Total should aggregate - assertEq(bank.balanceOf(address(bobWallet), address(usdc)), 300e18); - } - - // ── Multiple tokens ───────────────────────────────────────────── - - function test_multipleTokens_independentBalances() public { - vm.prank(depositor); - bank.deposit("tiktok", "alice", "alice", address(usdc), 300e18); - vm.prank(depositor); - bank.deposit("tiktok", "alice", "alice", address(dai), 500e18); - - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 300e18); - assertEq(bank.registeredBalances(address(aliceWallet), address(dai)), 500e18); - - _executeWithdraw(address(usdc), 100e18); - - assertEq(bank.registeredBalances(address(aliceWallet), address(usdc)), 200e18); - assertEq(bank.registeredBalances(address(aliceWallet), address(dai)), 500e18); - } - - // ═══════════════════════════════════════════════════════════════ - // MUTATION-GUARD NEGATIVE TESTS - // ═══════════════════════════════════════════════════════════════ - - // 14. Depositing an ERC20 while also sending native (msg.value > 0) → - // NativeSentWithErc20. depositor is approved, so removing the guard would - // succeed (pull the ERC20 anyway), killing the mutant. - function test_NativeSentWithErc20_reverts() public { - vm.deal(depositor, 1 ether); - vm.prank(depositor); - vm.expectRevert(NativeSentWithErc20.selector); - bank.deposit{value: 1}("tiktok", "alice", "alice", address(usdc), 100e18); - } - - // 15. Depositing an ERC20 without approving the diamond → the allowance guard - // fires with the custom InsufficientAllowance (removing it yields the OZ - // ERC20 transfer error). - function test_InsufficientAllowance_deposit_reverts() public { - address poor = makeAddr("poor"); - usdc.mint(poor, 100e18); // holds tokens but grants no allowance - // Parametrized error → match on the selector prefix (removing the guard - // yields the OZ ERC20 allowance error, a different selector). - vm.prank(poor); - vm.expectPartialRevert(InsufficientAllowance.selector); - bank.deposit("tiktok", "alice", "alice", address(usdc), 100e18); - } -} diff --git a/solidity/archived/transfer/test/EscrowById.t.sol b/solidity/archived/transfer/test/EscrowById.t.sol deleted file mode 100644 index f9e433d8..00000000 --- a/solidity/archived/transfer/test/EscrowById.t.sol +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IntegrationTest} from "./Integration.t.sol"; -import {Registry} from "../../login/Registry.sol"; -import {UserIdRequired} from "../bank/BankErrors.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; - -/// @notice Regression suite for the id-as-truth escrow invariant. -/// -/// Core invariant: unregistered escrow is keyed by the IMMUTABLE platform -/// user-id, domain-separated by the "id:v1" tag. The legacy handle-sweep -/// keyspace (keccak256(abi.encode(platform, handle))) must never collide with -/// it, so a wallet whose handle equals a victim's numeric id string cannot -/// sweep the victim's id-escrow. -contract EscrowByIdTest is IntegrationTest { - string constant XHOST = "api.x.com"; - - /// id-escrow slot key, mirroring Bank._escrowKey - /// (keccak256(abi.encode(ID_ESCROW_TAG, platform, userId))). - function _idKey(string memory platform, string memory userId) internal pure returns (bytes32) { - return keccak256(abi.encode("id:v1", platform, userId)); - } - - /// Legacy handle-keyed slot (pre-id deposits / handle sweep target). - function _handleKey(string memory platform, string memory handle) internal pure returns (bytes32) { - return keccak256(abi.encode(platform, handle)); - } - - /// Deposit `amount` into the id-escrow for (XHOST, userId). Handle is only - /// used for the (unregistered) resolve and never enters the escrow key. - function _fundId(string memory handle, string memory userId, uint256 amount) internal { - token.mint(address(this), amount); - token.approve(address(bank), amount); - bank.deposit(XHOST, handle, userId, address(token), amount); - } - - /// Register a fresh wallet for (handle, userId) with a distinct session key. - function _registerWallet(string memory handle, string memory userId, uint256 pk) internal returns (WebWallet) { - address sessionAddr = vm.addr(pk); - Registry.FullTlsProof memory proof = _buildRegistryProofWithId( - XHOST, handle, userId, "api.test.com/endpoint", '"username":"', sessionAddr, address(0), block.timestamp - ); - registry.register_session(proof, XHOST, handle, userId, "api.test.com/endpoint"); - return WebWallet(payable(registry.resolve(XHOST, handle))); - } - - /// Trigger lazy migration on `wallet` by depositing a tiny amount to its - /// (registered) identity. Returns nothing; side-effect is the sweep. - function _triggerMigrate(string memory handle, string memory userId) internal { - _fundId(handle, userId, 1); - } - - // ── Core invariant: escrow follows the id, not the handle ────────── - - /// Funds deposited to an unregistered id are swept to the wallet that later - /// registers that SAME id, even if the wallet's current handle differs. - function test_idEscrow_migratesByIdAcrossRename() public { - // Deposit to unregistered id "777" (handle at deposit time: "oldname"). - _fundId("oldname", "777", 100e18); - assertEq(bank.unregisteredBalances(_idKey(XHOST, "777"), address(token)), 100e18); - - // The same id "777" later registers under a DIFFERENT handle "newname". - WebWallet w = _registerWallet("newname", "777", 0xE001); - - // Migrate: id-escrow for "777" sweeps into the registered wallet. - _triggerMigrate("newname", "777"); - - assertEq(bank.unregisteredBalances(_idKey(XHOST, "777"), address(token)), 0, "id-escrow not swept"); - // 100e18 deposit + 1 wei migrate trigger. - assertEq(bank.registeredBalances(address(w), address(token)), 100e18 + 1, "registered balance wrong"); - } - - // ── C1 regression: id keyspace must not collide with handle sweep ── - - /// Fund id-escrow for (x, userId="12345"). Then register a DIFFERENT wallet - /// whose HANDLE is literally "12345" (with a different userId). Migrating it - /// must NOT sweep the victim's id-escrow. - function test_idEscrow_notStolenByHandleEqualToVictimId() public { - // Victim: 1000 tokens escrowed under the immutable id "12345". - _fundId("victim_handle", "12345", 1000e18); - bytes32 victimKey = _idKey(XHOST, "12345"); - assertEq(bank.unregisteredBalances(victimKey, address(token)), 1000e18); - - // Attacker registers a wallet whose HANDLE is the numeric string "12345" - // but whose immutable id is something else entirely. - WebWallet attacker = _registerWallet("12345", "id_attacker", 0xA77ACC); - - // Sanity: the attacker's handle-sweep key and the victim's id-key differ. - assertTrue(_handleKey(XHOST, "12345") != victimKey, "domain separation broken"); - - // Trigger migration on the attacker's wallet (handle == "12345"). - _triggerMigrate("12345", "id_attacker"); - - // The victim's id-escrow is UNTOUCHED — the attacker only swept its own - // (empty) handle slot and its own id slot. - assertEq(bank.unregisteredBalances(victimKey, address(token)), 1000e18, "victim id-escrow was swept (C1)"); - // Attacker only received its own 1-wei migrate trigger, nothing of the victim's. - assertEq(bank.registeredBalances(address(attacker), address(token)), 1, "attacker stole funds"); - } - - /// The victim can still claim their escrow by registering the real id. - function test_victimCanClaimAfterCollisionAttempt() public { - _fundId("victim_handle", "12345", 1000e18); - - // Attacker tries the handle-collision sweep first (no-op for the victim). - _registerWallet("12345", "id_attacker", 0xA77ACC); - _triggerMigrate("12345", "id_attacker"); - assertEq(bank.unregisteredBalances(_idKey(XHOST, "12345"), address(token)), 1000e18); - - // The legitimate owner of id "12345" registers (handle "real_owner"). - WebWallet victim = _registerWallet("real_owner", "12345", 0xC1A12); - _triggerMigrate("real_owner", "12345"); - - assertEq(bank.unregisteredBalances(_idKey(XHOST, "12345"), address(token)), 0, "victim could not claim"); - assertEq(bank.registeredBalances(address(victim), address(token)), 1000e18 + 1); - } - - // ── Deposit/read consistency through the id key ──────────────────── - - /// balanceOf(platform, handle, userId) reads the same id-escrow slot a - /// deposit with that userId writes — regardless of the handle string. - function test_balanceOf_readsIdEscrowConsistently() public { - _fundId("some_handle", "999", 42e18); - // Same id, different handle still reads the same slot. - assertEq(bank.balanceOf(XHOST, "some_handle", "999", address(token)), 42e18); - assertEq(bank.balanceOf(XHOST, "another_handle", "999", address(token)), 42e18); - // Different id reads an empty slot. - assertEq(bank.balanceOf(XHOST, "some_handle", "111", address(token)), 0); - } - - /// Empty userId is a write-only revert; reads of an unknown id return 0, - /// not a revert (no escrow slot to key). - function test_balanceOf_emptyId_returnsZero() public view { - assertEq(bank.balanceOf(XHOST, "ghost", "", address(token)), 0); - assertEq(bank.balanceOfTotal(XHOST, "ghost", "", address(token)), 0); - } - - /// Empty userId for an unregistered receiver reverts UserIdRequired. - function test_deposit_emptyIdForUnregistered_reverts() public { - token.mint(address(this), 1e18); - token.approve(address(bank), 1e18); - vm.expectRevert(UserIdRequired.selector); - bank.deposit(XHOST, "ghost", "", address(token), 1e18); - } -} diff --git a/solidity/archived/transfer/test/ImplicitTemplate.t.sol b/solidity/archived/transfer/test/ImplicitTemplate.t.sol deleted file mode 100644 index de513c8b..00000000 --- a/solidity/archived/transfer/test/ImplicitTemplate.t.sol +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {LibTemplate} from "../bank/libraries/LibTemplate.sol"; -import {LibRecipient} from "../bank/libraries/LibRecipient.sol"; -import {ReplyTargetIdNotQuoted} from "../bank/BankErrors.sol"; - -/// @dev Unit harness over the diamond's template/recipient libraries (same code -/// the diamond runs, its own diamond-storage slot). No legacy Bank. -contract BankTemplateHarness { - function addTemplate(string calldata platform, string calldata template) external { - LibTemplate.addTemplate(platform, template); - } - - /// @dev Reverts unless some template matches `body` AND extracts exactly - /// `amt`/`tok` (mirrors the on-chain webTransfer-by-name check). Returns - /// `recipientLess` — true when a recipient-less template matched (the - /// recipient is then bound off-body via the reply-target id leaf). - function checkByName( - bytes calldata body, - string calldata platform, - string calldata receiverHandle, - string calldata amt, - string calldata tok - ) external view returns (bool recipientLess) { - return LibTemplate.verifyTemplate(body, platform, receiverHandle, amt, tok); - } - - /// @dev Mirrors the `verifyProof` classification path: returns whether any - /// template matched and whether the match is recipient-less (derived from - /// the matched template SHAPE, not the extracted recipient value). - function matchAny(bytes calldata body, string calldata platform) - external - view - returns (bool matched, bool recipientLess) - { - (matched,,, recipientLess) = LibTemplate.matchAnyTemplate(body, keccak256(bytes(platform))); - } - - function extractReplyTarget(bytes calldata leaf, uint256 prefixLen) external pure returns (string memory) { - return LibRecipient.extractReplyTargetId(leaf, prefixLen); - } -} - -/// @notice Recipient-less honor templates: `@bot honor with {amount} {token}` -/// (recipient inferred off-chain from the reply/quote parent). Proves the -/// engine extracts amount/token without a `{recipient}` placeholder, flags -/// the match as recipient-less, the explicit form still works (and is not -/// flagged), and the two forms do not cross-match. -contract ImplicitTemplateTest is Test { - BankTemplateHarness h; - - function setUp() public { - h = new BankTemplateHarness(); - // Mirror initialize()'s X templates: explicit + recipient-less, "of" first. - h.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} of {token}"); - h.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} {token}"); - h.addTemplate("api.x.com", "@dyaka_agent honor with {amount} of {token}"); - h.addTemplate("api.x.com", "@dyaka_agent honor with {amount} {token}"); - } - - function test_implicit_withOf() public view { - // Recipient-less body: no receiverHandle in the body → flagged recipientLess. - assertTrue(h.checkByName(bytes("@dyaka_agent honor with 1.5 of $TIA"), "api.x.com", "", "1.5", "$TIA")); - } - - // ── reply-target id extraction is anchored to the field, not the last colon ── - // `"in_reply_to_user_id":` is 22 bytes. - uint256 constant PREFIX_LEN = 22; - - function test_extractReplyTarget_plain() public view { - assertEq(h.extractReplyTarget(bytes('"in_reply_to_user_id":"12345"'), PREFIX_LEN), "12345"); - } - - function test_extractReplyTarget_ignoresTrailingMentionId() public view { - // A longer revealed snippet whose LAST colon precedes an attacker id must - // still yield the reply target — not the trailing mention id. - bytes memory leaf = bytes('"in_reply_to_user_id":"victim","entities":{"mentions":[{"id":"attacker"'); - assertEq(h.extractReplyTarget(leaf, PREFIX_LEN), "victim"); - } - - function test_extractReplyTarget_rejectsUnquoted() public { - vm.expectRevert(ReplyTargetIdNotQuoted.selector); - h.extractReplyTarget(bytes('"in_reply_to_user_id":12345'), PREFIX_LEN); - } - - function test_implicit_withoutOf() public view { - assertTrue(h.checkByName(bytes("@dyaka_agent honor with 100 USDC"), "api.x.com", "", "100", "USDC")); - } - - function test_implicit_surroundingText() public view { - assertTrue( - h.checkByName(bytes("thanks! @dyaka_agent honor with 0.05 ETH keep it up"), "api.x.com", "", "0.05", "ETH") - ); - } - - function test_explicit_stillMatches_andIsNotRecipientLess() public view { - // Explicit body: recipient handle proven by the body must equal receiverHandle, - // and the match is NOT flagged recipient-less. - assertFalse(h.checkByName(bytes("@dyaka_agent honor @bob with 100 USDC"), "api.x.com", "bob", "100", "USDC")); - } - - function test_explicit_wrongHandle_reverts() public { - // Explicit body, but receiverHandle doesn't match the body's @bob → no match. - vm.expectRevert(); - h.checkByName(bytes("@dyaka_agent honor @bob with 100 USDC"), "api.x.com", "carol", "100", "USDC"); - } - - function test_wrongToken_reverts() public { - vm.expectRevert(); - h.checkByName(bytes("@dyaka_agent honor with 100 USDC"), "api.x.com", "", "100", "USDT"); - } - - // ── cross-matching isolation ─────────────────────────────────── - - // Only recipient-less templates registered -> an explicit-recipient body - // ("honor @bob with ...") must NOT match (prefix "honor with " absent). - function test_noCrossMatch_explicitBodyVsImplicitOnly() public { - BankTemplateHarness h2 = new BankTemplateHarness(); - h2.addTemplate("api.x.com", "@dyaka_agent honor with {amount} of {token}"); - h2.addTemplate("api.x.com", "@dyaka_agent honor with {amount} {token}"); - vm.expectRevert(); - h2.checkByName(bytes("@dyaka_agent honor @bob with 100 USDC"), "api.x.com", "bob", "100", "USDC"); - } - - // Only explicit templates registered -> a recipient-less body must NOT match - // (prefix "honor @" absent). - function test_noCrossMatch_implicitBodyVsExplicitOnly() public { - BankTemplateHarness h2 = new BankTemplateHarness(); - h2.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} of {token}"); - h2.addTemplate("api.x.com", "@dyaka_agent honor @{recipient} with {amount} {token}"); - vm.expectRevert(); - h2.checkByName(bytes("@dyaka_agent honor with 100 USDC"), "api.x.com", "", "100", "USDC"); - } - - // ── malformed explicit body (empty extracted recipient) ──────────── - // - // A body shaped like the EXPLICIT template ("honor @ …") but with an empty - // recipient must NOT be promoted to the recipient-less (reply-target-id-bound) - // path just because the extracted recipient string is empty. It is classified - // by the matched template SHAPE, which has a {recipient} placeholder. - - // by-name path: an explicit template extracting an empty recipient does not - // early-accept as recipient-less; it falls through to the handle check, which - // fails for the empty recipient vs the real intended handle → reverts (rejected). - function test_malformedExplicitBody_rejected_byName() public { - vm.expectRevert(); - h.checkByName(bytes("@dyaka_agent honor @ with 5 $TST"), "api.x.com", "bob", "5", "$TST"); - } - - // verifyProof classification path: the malformed explicit body matches but is - // NOT flagged recipient-less, so verifyProof would not require/accept the - // reply-target id leaf for it. - function test_malformedExplicitBody_notRecipientLess_matchAny() public view { - (bool matched, bool recipientLess) = h.matchAny(bytes("@dyaka_agent honor @ with 5 $TST"), "api.x.com"); - assertTrue(matched, "malformed explicit body still matches an explicit template"); - assertFalse(recipientLess, "malformed explicit body must NOT be classified recipient-less"); - } - - // Regression guard: a genuine recipient-less body (no {recipient} in the - // template shape) IS still classified recipient-less by the same path. - function test_genuineImplicitBody_isRecipientLess_matchAny() public view { - (bool matched, bool recipientLess) = h.matchAny(bytes("@dyaka_agent honor with 100 USDC"), "api.x.com"); - assertTrue(matched, "genuine recipient-less body matches"); - assertTrue(recipientLess, "genuine recipient-less body must be classified recipient-less"); - } -} diff --git a/solidity/archived/transfer/test/Integration.t.sol b/solidity/archived/transfer/test/Integration.t.sol deleted file mode 100644 index 06841c83..00000000 --- a/solidity/archived/transfer/test/Integration.t.sol +++ /dev/null @@ -1,2720 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../login/Registry.sol"; -import {IBank} from "../bank/IBank.sol"; -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "../bank/BankTypes.sol"; -import { - InvalidMerkleProof, - InvalidBackendSignature, - TransferAlreadyUsed, - SenderNotRegistered, - ZeroAmount, - TokenNotRegistered, - NoTemplateMatch, - AmountWeiMismatch, - ReplyTargetMismatch, - RecipientLessNeedsReplyTarget, - QuotedAuthorDifferentObject, - QuotedAuthorMismatch, - QuotedAuthorMissingQuotedId, - RefNotQuote, - QuotedAuthorLeafNotIncludes, - UnknownResourceType, - RequestPathMismatch, - AuthorNotInRevealedField, - TemplateNotFound, - InvalidNotarySignature, - DomainHashMismatch, - MerklePathTooLong, - ZeroToken, - SourceUrlNotHttps, - SourceUrlDomainMismatch, - ReplyLeafTooShort, - ReplyLeafNotInReplyTo, - QuotedRefTooShort, - QuotedRefNotObject, - QuotedRefNotFlatObject, - QuotedRefMissingId, - QuotedAuthorLeafTooShort, - QuotedAuthorMissingAuthorId, - EmptyQuotedValue -} from "../bank/BankErrors.sol"; -import {LibString} from "../bank/libraries/LibString.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {WalletFactory} from "../../login/WalletFactory.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; -import {MockERC20} from "../MockERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -/// @dev V2 mock — just adds version() returning 2. -contract MockWebWalletV2 is WebWallet { - function version() external pure returns (uint256) { - return 2; - } -} - -contract IntegrationTest is Test, BankDiamondDeployer { - // ─── Known test keys ──────────────────────────────────────────── - uint256 constant NOTARY_KEY = uint256(keccak256("notary")); - uint256 constant BACKEND_KEY = uint256(keccak256("backend")); - - address NOTARY; - address BACKEND; - - // ─── Contracts ────────────────────────────────────────────────── - Registry registry; - IBank bank; - WalletFactory factory; - MockERC20 token; - - address owner; - - // ─── Named session keys ───────────────────────────────────────── - uint256 constant ALICE_PK = 0xA001; - uint256 constant ALICE_PK2 = 0xA002; - uint256 constant ALICE_PK3 = 0xA003; - uint256 constant BOB_PK = 0xB001; - uint256 constant CHARLIE_PK = 0xC001; - uint256 constant LINK_PK1 = 0xD001; - uint256 constant LINK_PK2 = 0xD002; - uint256 constant LINK_PK3 = 0xD003; - - // ─── Transfer counter for unique UIDs ─────────────────────────── - uint256 private _transferCounter; - - // ─── setUp ────────────────────────────────────────────────────── - - function setUp() public { - NOTARY = vm.addr(NOTARY_KEY); - BACKEND = vm.addr(BACKEND_KEY); - owner = address(this); - - WebWallet walletImpl = new WebWallet(); - Registry rImpl = new Registry(); - WalletFactory fImpl = new WalletFactory(); - - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), abi.encodeCall(WalletFactory.initialize, (owner, address(walletImpl), address(1))) - ) - ) - ); - - Notary notaryReg = deployNotary(owner, NOTARY); - - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall(Registry.initialize, (address(notaryReg), BACKEND, address(factory), owner)) - ) - ) - ); - - factory.setRegistry(address(registry)); - - // Bank is now an EIP-2535 diamond (BankInit seeds the same templates/prefixes). - bank = IBank(deployBankDiamond(owner, address(notaryReg), BACKEND, address(registry))); - - token = new MockERC20("Test", "TST"); - bank.registerToken("TST", address(token)); - - // Register platforms used in tests (X-style quoted id binding). - registry.setPlatform("api.x.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - registry.setPlatform("api.github.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - - vm.warp(1_000_000); - } - - /// Deterministic immutable id for a handle (tests key escrow by id). - function _idFor(string memory handle) internal pure returns (string memory) { - return string(abi.encodePacked("id_", handle)); - } - - // ═══════════════════════════════════════════════════════════════ - // FLUENT HELPERS - // ═══════════════════════════════════════════════════════════════ - - // ─── Wallet creation ──────────────────────────────────────────── - - function _alice() internal returns (WebWallet) { - return _walletFor("x", "alice", ALICE_PK); - } - - function _bob() internal returns (WebWallet) { - return _walletFor("x", "bob", BOB_PK); - } - - function _charlie() internal returns (WebWallet) { - return _walletFor("x", "charlie", CHARLIE_PK); - } - - /// Map platform shortname to API domain for Registry registration. - /// In production, OAuth registers under the TLS domain (e.g. "api.x.com"). - /// Bank.webTransfer uses the apiHost to resolve the sender in the Registry. - function _domain(string memory platform) internal pure returns (string memory) { - if (keccak256(bytes(platform)) == keccak256("x")) return "api.x.com"; - if (keccak256(bytes(platform)) == keccak256("github")) return "api.github.com"; - return platform; - } - - function _walletFor(string memory platform, string memory handle, uint256 pk) internal returns (WebWallet) { - string memory domain = _domain(platform); - string memory userId = _idFor(handle); - address sessionAddr = vm.addr(pk); - Registry.FullTlsProof memory proof = _buildRegistryProofWithId( - domain, handle, userId, "api.test.com/endpoint", '"username":"', sessionAddr, address(0), block.timestamp - ); - registry.register_session(proof, domain, handle, userId, "api.test.com/endpoint"); - return WebWallet(payable(registry.resolve(domain, handle))); - } - - // ─── Funding ──────────────────────────────────────────────────── - - function _fund(string memory platform, string memory handle, uint256 amount) internal { - token.mint(address(this), amount); - token.approve(address(bank), amount); - bank.deposit(_domain(platform), handle, _idFor(handle), address(token), amount); - } - - function _fundWallet(WebWallet wallet, uint256 amount) internal { - token.mint(address(wallet), amount); - } - - // ─── Transfers ────────────────────────────────────────────────── - - function _transfer( - string memory senderPlatform, - string memory senderHandle, - string memory receiverPlatform, - string memory receiverHandle, - uint256 amount - ) internal returns (bytes32 uid) { - // Convert wei to human-readable: amount / 1e18 as integer string - string memory amountStr = vm.toString(amount / 1e18); - return _transferWithAmountStr(senderPlatform, senderHandle, receiverPlatform, receiverHandle, amountStr, amount); - } - - function _transferWithAmountStr( - string memory senderPlatform, - string memory senderHandle, - string memory receiverPlatform, - string memory receiverHandle, - string memory amountStr, - uint256 amountWei - ) internal returns (bytes32 uid) { - _transferCounter++; - string memory resourceId = string(abi.encodePacked("repo/", vm.toString(_transferCounter), "/comment/1")); - string memory requestPath = - string(abi.encodePacked("/repos/owner/repo/issues/comments/", vm.toString(_transferCounter))); - - uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - - // Selective disclosure: body = just the comment text, author = separate field. - // id-only: the sender's author-id is always proven (id-path). - bytes memory body = abi.encodePacked("@dyaka-agent honor @", receiverHandle, " with ", amountStr, " TST"); - bytes memory authorBytes = bytes(senderHandle); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor(senderHandle), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor(receiverHandle), block.timestamp); - - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - bank.webTransferV2( - resource, - SenderInfo({author: senderHandle, revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - receiverHandle, - _idFor(receiverHandle), - "TST", - amountStr, - amountWei, - "" - ); - } - - /// Security regression: a forged revealedAuthorId (victim id, not Merkle-proven) - /// is rejected by _verifyRecvLeaf — no unsigned id can redirect resolveById. - function test_webTransfer_forgedAuthorId_reverts() public { - _walletFor("github", "alice", ALICE_PK); // attacker (has a valid proof of own comment) - _walletFor("github", "bob", BOB_PK); // victim - _fund("github", "bob", 1000e18); - - string memory resourceId = "repo/evil/comment/1"; - string memory requestPath = "/repos/owner/repo/issues/comments/777"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @attacker with 400 TST"); - bytes memory authorBytes = bytes("alice"); - - NotaryTlsProof memory notaryProof = - _buildBankProof("api.github.com", requestPath, body, authorBytes, block.timestamp); - BackendSig memory backendSig = _buildBackendSig(uid, body, authorBytes, _idFor("attacker"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - // Forged author-id snippet pointing at the victim, with NO valid - // authorIdMerklePath (the proof above has none) → _verifyRecvLeaf rejects. - bytes memory forgedId = abi.encodePacked('"id":', _idFor("bob"), ","); - vm.expectRevert(InvalidMerkleProof.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: forgedId}), - body, - notaryProof, - backendSig, - "attacker", - _idFor("attacker"), - "TST", - "400", - 400e18, - "" - ); - } - - // C1: the proven body honors bob, but a front-runner swaps receiverHandle to - // their own identity. The on-chain recipient==receiverHandle bind rejects it. - function test_webTransfer_recipientSwap_reverts() public { - string memory resourceId = "repo/x/comment/1"; - string memory requestPath = "/repos/owner/repo/issues/comments/1"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 TST"); - bytes memory authorBytes = bytes("alice"); - - NotaryTlsProof memory notaryProof = - _buildBankProof("api.github.com", requestPath, body, authorBytes, block.timestamp); - // Backend signed for the real recipient bob. - BackendSig memory backendSig = _buildBackendSig(uid, body, authorBytes, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(NoTemplateMatch.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: ""}), - body, - notaryProof, - backendSig, - "attacker", // swapped — body says @bob - _idFor("attacker"), - "TST", - "400", - 400e18, - "" - ); - } - - // C1: front-runner keeps receiverHandle=bob (matches body) but swaps the - // receiverUserId. It is bound into the backend digest -> signature rejects it. - function test_webTransfer_receiverIdSwap_reverts() public { - string memory resourceId = "repo/x/comment/2"; - string memory requestPath = "/repos/owner/repo/issues/comments/2"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - // Backend signed for the REAL receiver bob; the call swaps in attacker's id. - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(InvalidBackendSignature.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("attacker"), // swapped id — bound in the digest - "TST", - "400", - 400e18, - "" - ); - } - - /// @dev Transfer with a specific uid (for replay testing) - function _transferWithUid( - string memory resourceId, - string memory senderPlatform, - string memory senderHandle, - string memory receiverPlatform, - string memory receiverHandle, - uint256 amount - ) internal { - string memory amountStr = vm.toString(amount / 1e18); - _transferWithUidAndAmountStr( - resourceId, senderPlatform, senderHandle, receiverPlatform, receiverHandle, amountStr, amount - ); - } - - function _transferWithUidAndAmountStr( - string memory resourceId, - string memory senderPlatform, - string memory senderHandle, - string memory receiverPlatform, - string memory receiverHandle, - string memory amountStr, - uint256 amountWei - ) internal { - string memory requestPath = "/repos/owner/repo/issues/comments/fixed"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - - bytes memory body = abi.encodePacked("@dyaka-agent honor @", receiverHandle, " with ", amountStr, " TST"); - bytes memory authorBytes = bytes(senderHandle); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor(senderHandle), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor(receiverHandle), block.timestamp); - - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - bank.webTransferV2( - resource, - SenderInfo({author: senderHandle, revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - receiverHandle, - _idFor(receiverHandle), - "TST", - amountStr, - amountWei, - "" - ); - } - - // ─── Execute helpers ──────────────────────────────────────────── - - function _execute(WebWallet wallet, address target, bytes memory data, uint256 pk) internal { - uint256[] memory pks = new uint256[](1); - pks[0] = pk; - _executeMulti(wallet, target, 0, data, pks); - } - - function _executeMulti(WebWallet wallet, address target, uint256 value, bytes memory data, uint256[] memory pks) - internal - { - uint256 currentNonce = wallet.nonce(); - bytes32 digest = - keccak256(abi.encode(target, value, keccak256(data), currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](pks.length); - for (uint256 i = 0; i < pks.length; i++) { - sigs[i] = _sign(pks[i], digest); - } - wallet.execute(target, value, data, sigs); - } - - function _executeBatch( - WebWallet wallet, - address[] memory targets, - uint256[] memory values, - bytes[] memory datas, - uint256[] memory pks - ) internal { - uint256 currentNonce = wallet.nonce(); - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = - keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](pks.length); - for (uint256 i = 0; i < pks.length; i++) { - sigs[i] = _sign(pks[i], digest); - } - wallet.executeBatch(targets, values, datas, sigs); - } - - // ─── Assertions ───────────────────────────────────────────────── - - function _assertBankBalance(string memory platform, string memory handle, uint256 expected) internal view { - assertEq(bank.balanceOf(_domain(platform), handle, _idFor(handle), address(token)), expected); - } - - function _assertBankBalance(WebWallet wallet, uint256 expected) internal view { - assertEq(bank.balanceOf(address(wallet), address(token)), expected); - } - - function _assertTokenBalance(WebWallet wallet, uint256 expected) internal view { - assertEq(token.balanceOf(address(wallet)), expected); - } - - function _assertUnregisteredBalance(string memory platform, string memory handle, uint256 expected) internal view { - bytes32 key = keccak256(abi.encode("id:v1", _domain(platform), _idFor(handle))); - assertEq(bank.unregisteredBalances(key, address(token)), expected); - } - - function _assertRegisteredBalance(WebWallet wallet, uint256 expected) internal view { - assertEq(bank.registeredBalances(address(wallet), address(token)), expected); - } - - // ─── Identity helpers ─────────────────────────────────────────── - - function _linkIdentity(WebWallet wallet, string memory platform, string memory handle, uint256 signerPk) internal { - _linkIdentityWithSessionPk(wallet, platform, handle, signerPk, signerPk); - } - - function _linkIdentityWithSessionPk( - WebWallet wallet, - string memory platform, - string memory handle, - uint256 signerPk, - uint256 newSessionPk - ) internal { - string memory domain = _domain(platform); - string memory userId = _idFor(handle); - Registry.FullTlsProof memory proof = _buildRegistryProofWithId( - domain, - handle, - userId, - "api.test.com/endpoint", - '"username":"', - vm.addr(newSessionPk), - address(wallet), - block.timestamp - ); - bytes memory data = - abi.encodeCall(Registry.linkIdentity, (domain, handle, userId, proof, "api.test.com/endpoint")); - _execute(wallet, address(registry), data, signerPk); - } - - function _addSession(string memory platform, string memory handle, uint256 pk) internal { - string memory domain = _domain(platform); - string memory userId = _idFor(handle); - address sessionAddr = vm.addr(pk); - Registry.FullTlsProof memory proof = _buildRegistryProofWithId( - domain, handle, userId, "api.test.com/endpoint", '"username":"', sessionAddr, address(0), block.timestamp - ); - registry.register_session(proof, domain, handle, userId, "api.test.com/endpoint"); - } - - function _setThreshold(WebWallet wallet, uint8 t, uint256 signerPk) internal { - bytes memory data = abi.encodeCall(WebWallet.setThreshold, (t)); - _execute(wallet, address(wallet), data, signerPk); - } - - function _revokeSession( - WebWallet wallet, - string memory platform, - string memory handle, - address sessionAddr, - uint256 signerPk - ) internal { - bytes memory data = abi.encodeCall( - WebWallet.revokeSession, (_domain(platform), handle, _idFor(handle), sessionAddr) - ); - _execute(wallet, address(wallet), data, signerPk); - } - - // ═══════════════════════════════════════════════════════════════ - // LOW-LEVEL PROOF BUILDERS - // ═══════════════════════════════════════════════════════════════ - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _buildPaths(bytes memory domainBytes, bytes memory usernameBytes, bytes memory endpointBytes) - internal - pure - returns ( - bytes32 transcriptRoot, - bytes32[] memory domainPath, - bytes32[] memory usernamePath, - bytes32[] memory endpointPath - ) - { - bytes32 leafD = _leaf("domain:", domainBytes); - bytes32 leafU = _leaf("recv:", usernameBytes); - bytes32 leafE = _leaf("endpoint:", endpointBytes); - - bytes32 hDu = - leafD < leafU ? keccak256(abi.encodePacked(leafD, leafU)) : keccak256(abi.encodePacked(leafU, leafD)); - - transcriptRoot = hDu < leafE ? keccak256(abi.encodePacked(hDu, leafE)) : keccak256(abi.encodePacked(leafE, hDu)); - - domainPath = new bytes32[](2); - domainPath[0] = leafU; - domainPath[1] = leafE; - - usernamePath = new bytes32[](2); - usernamePath[0] = leafD; - usernamePath[1] = leafE; - - endpointPath = new bytes32[](1); - endpointPath[0] = hDu; - } - - function _buildRegistryProof( - string memory domain, - string memory username, - string memory endpoint, - string memory handlePrefix, - address userAddress, - uint256 ts - ) internal view returns (Registry.FullTlsProof memory proof) { - return _buildRegistryProofForWallet(domain, username, endpoint, handlePrefix, userAddress, address(0), ts); - } - - function _buildRegistryProofForWallet( - string memory domain, - string memory username, - string memory endpoint, - string memory handlePrefix, - address userAddress, - address walletAddress, - uint256 ts - ) internal view returns (Registry.FullTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes memory usernameSnippet = abi.encodePacked(handlePrefix, username, '"'); - (bytes32 transcriptRoot, bytes32[] memory dp, bytes32[] memory up, bytes32[] memory ep) = - _buildPaths(bytes(domain), usernameSnippet, bytes(endpoint)); - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(userAddress, walletAddress, transcriptRoot, ts)); - - proof = Registry.FullTlsProof({ - notarySignature: _sign(NOTARY_KEY, notaryDigest), - backendSignature: _sign(BACKEND_KEY, backendDigest), - userAddress: userAddress, - walletAddress: walletAddress, - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: new bytes32[](0) - }); - } - - // 4-leaf tree (D, U, E, Id) — id bound via the quoted "id":"" recv leaf. - function _buildRegistryProofWithId( - string memory domain, - string memory username, - string memory userId, - string memory endpoint, - string memory handlePrefix, - address userAddress, - address walletAddress, - uint256 ts - ) internal view returns (Registry.FullTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes32 leafD = _leaf("domain:", bytes(domain)); - bytes32 leafU = _leaf("recv:", abi.encodePacked(handlePrefix, username, '"')); - bytes32 leafE = _leaf("endpoint:", bytes(endpoint)); - bytes32 leafId = _leaf("recv:", abi.encodePacked('"id":"', userId, '"')); - - bytes32 hDU = _h(leafD, leafU); - bytes32 hEId = _h(leafE, leafId); - bytes32 root = _h(hDU, hEId); - - bytes32[] memory dp = new bytes32[](2); - dp[0] = leafU; - dp[1] = hEId; - bytes32[] memory up = new bytes32[](2); - up[0] = leafD; - up[1] = hEId; - bytes32[] memory ep = new bytes32[](2); - ep[0] = leafId; - ep[1] = hDU; - bytes32[] memory idp = new bytes32[](2); - idp[0] = leafE; - idp[1] = hDU; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - root, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(userAddress, walletAddress, root, ts)); - - proof = Registry.FullTlsProof({ - notarySignature: _sign(NOTARY_KEY, notaryDigest), - backendSignature: _sign(BACKEND_KEY, backendDigest), - userAddress: userAddress, - walletAddress: walletAddress, - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: root, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: idp - }); - } - - function _h(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - function _buildBankProof( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(333)); - bytes32 serverRandom = bytes32(uint256(444)); - bytes memory serverEphKey = hex"cafebabe"; - bytes32 domainHash = keccak256(bytes(apiHost)); - - // 4 leaves: domain, endpoint, body (recv), author (recv) - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - - // Build Merkle tree from [leafD, leafE, leafB, leafA] - bytes32 hDE = - leafD < leafE ? keccak256(abi.encodePacked(leafD, leafE)) : keccak256(abi.encodePacked(leafE, leafD)); - - bytes32 hBA = - leafB < leafA ? keccak256(abi.encodePacked(leafB, leafA)) : keccak256(abi.encodePacked(leafA, leafB)); - - bytes32 transcriptRoot = - hDE < hBA ? keccak256(abi.encodePacked(hDE, hBA)) : keccak256(abi.encodePacked(hBA, hDE)); - - // Merkle paths for each leaf (sibling + uncle) - bytes32[] memory bodyPath = new bytes32[](2); - bodyPath[0] = leafA; // sibling - bodyPath[1] = hDE; // uncle - - bytes32[] memory authorPath = new bytes32[](2); - authorPath[0] = leafB; // sibling - authorPath[1] = hDE; // uncle - - bytes32[] memory domainPath = new bytes32[](2); - domainPath[0] = leafE; // sibling - domainPath[1] = hBA; // uncle - - bytes32[] memory endpointPath = new bytes32[](2); - endpointPath[0] = leafD; // sibling - endpointPath[1] = hBA; // uncle - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - - proof = NotaryTlsProof({ - notarySig: _sign(NOTARY_KEY, notaryDigest), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - bodyMerklePath: bodyPath, - authorMerklePath: authorPath, - domainMerklePath: domainPath, - endpointMerklePath: endpointPath, - authorIdMerklePath: new bytes32[](0), - receiverIdMerklePath: new bytes32[](0), - revealedReceiverId: "", - quotedRefMerklePath: new bytes32[](0), - revealedQuotedRef: "", - quotedAuthorMerklePath: new bytes32[](0), - revealedQuotedAuthorId: "" - }); - } - - function _hp(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - /// 5-leaf proof builder (adds the author-id recv leaf) so the id-resolution - /// path can be exercised on-chain. The id leaf is appended at the tree top: - /// its path is the old 4-leaf root, and the other leaves' paths gain it. - function _buildBankProofWithId( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(333)); - bytes32 serverRandom = bytes32(uint256(444)); - bytes memory serverEphKey = hex"cafebabe"; - bytes32 domainHash = keccak256(bytes(apiHost)); - - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - bytes32 leafId = _leaf("recv:", revealedAuthorId); - - bytes32 hDE = _hp(leafD, leafE); - bytes32 hBA = _hp(leafB, leafA); - bytes32 root4 = _hp(hDE, hBA); - bytes32 transcriptRoot = _hp(root4, leafId); - - bytes32[] memory bodyPath = new bytes32[](3); - (bodyPath[0], bodyPath[1], bodyPath[2]) = (leafA, hDE, leafId); - bytes32[] memory authorPath = new bytes32[](3); - (authorPath[0], authorPath[1], authorPath[2]) = (leafB, hDE, leafId); - bytes32[] memory domainPath = new bytes32[](3); - (domainPath[0], domainPath[1], domainPath[2]) = (leafE, hBA, leafId); - bytes32[] memory endpointPath = new bytes32[](3); - (endpointPath[0], endpointPath[1], endpointPath[2]) = (leafD, hBA, leafId); - bytes32[] memory authorIdPath = new bytes32[](1); - authorIdPath[0] = root4; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - - proof = NotaryTlsProof({ - notarySig: _sign(NOTARY_KEY, notaryDigest), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - bodyMerklePath: bodyPath, - authorMerklePath: authorPath, - domainMerklePath: domainPath, - endpointMerklePath: endpointPath, - authorIdMerklePath: authorIdPath, - receiverIdMerklePath: new bytes32[](0), - revealedReceiverId: "", - quotedRefMerklePath: new bytes32[](0), - revealedQuotedRef: "", - quotedAuthorMerklePath: new bytes32[](0), - revealedQuotedAuthorId: "" - }); - } - - function _buildBackendSigWithId( - bytes32 uid, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - string memory receiverUserId, - uint256 ts - ) internal pure returns (BackendSig memory) { - bytes32 digest = keccak256( - abi.encodePacked(uid, revealedBody, revealedAuthor, revealedAuthorId, keccak256(bytes(receiverUserId)), ts) - ); - return BackendSig({sig: _sign(BACKEND_KEY, digest), timestamp: ts}); - } - - // Happy path: a non-empty proven revealedAuthorId resolves the sender via - // _extractId -> resolveById (not the handle), and the transfer settles. - function test_webTransfer_idPath_resolvesByExtractedId() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - string memory resourceId = "repo/x/comment/idpath"; - string memory requestPath = "/repos/owner/repo/issues/comments/5"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - uint256 aliceBefore = bank.balanceOf("api.github.com", "alice", _idFor("alice"), address(token)); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - - assertEq(bank.balanceOf("api.github.com", "alice", _idFor("alice"), address(token)), aliceBefore - 400e18); - assertEq(bank.balanceOf("api.github.com", "bob", _idFor("bob"), address(token)), 400e18); - } - - // ── recipient-less (reply-inferred) honor: recipient bound by the proven - // `in_reply_to_user_id` leaf, NOT by the body ───────────────────────── - - // 6-leaf proof builder: adds the reply-target recv leaf alongside author-id. - // `_hp` is commutative, so sibling order within a level does not matter. - function _buildBankProofWithReceiverId( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - bytes memory revealedReceiverId, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(333)); - bytes32 serverRandom = bytes32(uint256(444)); - bytes memory serverEphKey = hex"cafebabe"; - bytes32 domainHash = keccak256(bytes(apiHost)); - - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - bytes32 leafId = _leaf("recv:", revealedAuthorId); - bytes32 leafR = _leaf("recv:", revealedReceiverId); - - bytes32 hDE = _hp(leafD, leafE); - bytes32 hBA = _hp(leafB, leafA); - bytes32 root4 = _hp(hDE, hBA); - bytes32 hIdR = _hp(leafId, leafR); - bytes32 transcriptRoot = _hp(root4, hIdR); - - bytes32[] memory bodyPath = new bytes32[](3); - (bodyPath[0], bodyPath[1], bodyPath[2]) = (leafA, hDE, hIdR); - bytes32[] memory authorPath = new bytes32[](3); - (authorPath[0], authorPath[1], authorPath[2]) = (leafB, hDE, hIdR); - bytes32[] memory domainPath = new bytes32[](3); - (domainPath[0], domainPath[1], domainPath[2]) = (leafE, hBA, hIdR); - bytes32[] memory endpointPath = new bytes32[](3); - (endpointPath[0], endpointPath[1], endpointPath[2]) = (leafD, hBA, hIdR); - bytes32[] memory authorIdPath = new bytes32[](2); - (authorIdPath[0], authorIdPath[1]) = (leafR, root4); - bytes32[] memory receiverIdPath = new bytes32[](2); - (receiverIdPath[0], receiverIdPath[1]) = (leafId, root4); - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - - proof = NotaryTlsProof({ - notarySig: _sign(NOTARY_KEY, notaryDigest), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - bodyMerklePath: bodyPath, - authorMerklePath: authorPath, - domainMerklePath: domainPath, - endpointMerklePath: endpointPath, - authorIdMerklePath: authorIdPath, - receiverIdMerklePath: receiverIdPath, - revealedReceiverId: revealedReceiverId, - quotedRefMerklePath: new bytes32[](0), - revealedQuotedRef: "", - quotedAuthorMerklePath: new bytes32[](0), - revealedQuotedAuthorId: "" - }); - } - - // Happy path: a recipient-less body ("honor with ...", no @recipient) settles - // to the id proven by the notarized `in_reply_to_user_id` leaf. - function test_webTransfer_recipientLess_bindsProvenReplyTarget() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-implicit-1"; - string memory requestPath = "/2/tweets?ids=tweet-implicit-1"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - // X prepends the parent @bob; the command itself carries no @recipient. - bytes memory body = abi.encodePacked("@bob @dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory receiverId = abi.encodePacked('"in_reply_to_user_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", requestPath, body, authorBytes, authorId, receiverId, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - uint256 aliceBefore = bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", // display handle (reply parent); recipient bound by the leaf, not the body - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - - assertEq(bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)), aliceBefore - 400e18); - assertEq(bank.balanceOf("api.x.com", "bob", _idFor("bob"), address(token)), 400e18); - } - - // The proven reply target (carol) must equal the backend-supplied receiverUserId - // (bob) — a swapped receiver id reverts even with a valid leaf. - function test_webTransfer_recipientLess_forgedReplyTarget_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-implicit-2"; - string memory requestPath = "/2/tweets?ids=tweet-implicit-2"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@carol @dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - // Leaf proves carol is the reply target... - bytes memory receiverId = abi.encodePacked('"in_reply_to_user_id":"', _idFor("carol"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", requestPath, body, authorBytes, authorId, receiverId, block.timestamp - ); - // ...but the backend signs bob as the receiver id → mismatch. - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(ReplyTargetMismatch.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // A recipient-less body with NO proven reply target (empty receiver-id leaf, - // e.g. a standalone tweet) must revert — recipient-less honors are reply-only. - function test_webTransfer_recipientLess_noReplyTarget_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-implicit-3"; - string memory requestPath = "/2/tweets?ids=tweet-implicit-3"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - // 5-leaf builder → revealedReceiverId "" and empty receiverIdMerklePath. - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.x.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(RecipientLessNeedsReplyTarget.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // Preflight parity: verifyProof MUST reject a recipient-less body with no proven - // reply target (empty receiver-id leaf) — the same way webTransferV2 does — - // instead of green-lighting a proof the transfer would then revert on. - function test_verifyProof_recipientLess_noReplyTarget_reverts() public { - string memory resourceId = "tweet-implicit-vp"; - string memory requestPath = "/2/tweets?ids=tweet-implicit-vp"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - // 5-leaf builder → revealedReceiverId "" and empty receiverIdMerklePath. - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.x.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(RecipientLessNeedsReplyTarget.selector); - bank.verifyProof( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // With a valid proven reply-target leaf matching the receiver id, verifyProof - // returns true (the recipient-less gate must not over-reject the happy path). - function test_verifyProof_recipientLess_validReplyTarget_ok() public { - string memory resourceId = "tweet-implicit-vp2"; - string memory requestPath = "/2/tweets?ids=tweet-implicit-vp2"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@bob @dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory receiverId = abi.encodePacked('"in_reply_to_user_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", requestPath, body, authorBytes, authorId, receiverId, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - assertTrue( - bank.verifyProof( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - _idFor("bob") - ) - ); - } - - // ── recipient-less (quote-inferred) honor: recipient = author of the quoted - // tweet, joined across the quoted-reference and includes-author leaves ──── - - // 8-leaf-shaped proof builder: adds the quoted-reference and quoted-author recv - // leaves (the quoted-author is self-paired to fill the tree). `_hp` is - // commutative, so sibling order within a level does not matter. - function _buildBankProofWithQuotedAuthor( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - bytes memory revealedQuotedRef, - bytes memory revealedQuotedAuthorId, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 domainHash = keccak256(bytes(apiHost)); - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - bytes32 leafId = _leaf("recv:", revealedAuthorId); - bytes32 leafQr = _leaf("recv:", revealedQuotedRef); - bytes32 leafQa = _leaf("recv:", revealedQuotedAuthorId); - - bytes32 hDE = _hp(leafD, leafE); - bytes32 hBA = _hp(leafB, leafA); - bytes32 root4 = _hp(hDE, hBA); - bytes32 hIdQr = _hp(leafId, leafQr); - bytes32 hQaQa = _hp(leafQa, leafQa); // pad: quoted-author self-paired - bytes32 rightSub = _hp(hIdQr, hQaQa); - bytes32 transcriptRoot = _hp(root4, rightSub); - - proof.notarySig = _sign( - NOTARY_KEY, - keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - bytes32(uint256(333)), - bytes32(uint256(444)), - keccak256(hex"cafebabe"), - transcriptRoot, - ts - ) - ) - ); - proof.domainHash = domainHash; - proof.clientRandom = bytes32(uint256(333)); - proof.serverRandom = bytes32(uint256(444)); - proof.serverEphemeralKey = hex"cafebabe"; - proof.transcriptRoot = transcriptRoot; - proof.timestamp = ts; - proof.bodyMerklePath = _path3(leafA, hDE, rightSub); - proof.authorMerklePath = _path3(leafB, hDE, rightSub); - proof.domainMerklePath = _path3(leafE, hBA, rightSub); - proof.endpointMerklePath = _path3(leafD, hBA, rightSub); - proof.authorIdMerklePath = _path3(leafQr, hQaQa, root4); - proof.receiverIdMerklePath = new bytes32[](0); - proof.revealedReceiverId = ""; - proof.quotedRefMerklePath = _path3(leafId, hQaQa, root4); - proof.revealedQuotedRef = revealedQuotedRef; - proof.quotedAuthorMerklePath = _path3(leafQa, hIdQr, root4); - proof.revealedQuotedAuthorId = revealedQuotedAuthorId; - } - - function _path3(bytes32 a, bytes32 b, bytes32 c) internal pure returns (bytes32[] memory p) { - p = new bytes32[](3); - (p[0], p[1], p[2]) = (a, b, c); - } - - // Happy path: a recipient-less body ("honor with ...", no @recipient) on a QUOTE - // tweet settles to the author of the quoted tweet, proven by the quoted-reference - // + includes-author leaves. - function test_webTransfer_recipientLess_bindsQuotedAuthor() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-1"; - string memory requestPath = "/2/tweets?ids=tweet-quote-1"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - // bob authored the quoted tweet "qt-99". - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - uint256 aliceBefore = bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", // display handle; recipient bound by the quoted-author leaf, not the body - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - - assertEq(bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)), aliceBefore - 400e18); - assertEq(bank.balanceOf("api.x.com", "bob", _idFor("bob"), address(token)), 400e18); - } - - // Real X emits `referenced_tweets` objects id-FIRST: `{"id":…,"type":"quoted"}`. - // The quote binding must be field-order-independent — this is the exact shape - // the live quote honor produces (the type-first happy path above proves the - // other order). Regression for the on-chain half of the live quote fix. - function test_webTransfer_recipientLess_bindsQuotedAuthor_idFirstOrder() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-idfirst"; - string memory requestPath = "/2/tweets?ids=tweet-quote-idfirst"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - // id-FIRST, as X actually serializes it. - bytes memory quotedRef = abi.encodePacked('{"id":"qt-99","type":"quoted"}'); - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - uint256 aliceBefore = bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - - assertEq(bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)), aliceBefore - 400e18); - assertEq(bank.balanceOf("api.x.com", "bob", _idFor("bob"), address(token)), 400e18); - } - - // L6: the extracted author_id must sit in the SAME includes-tweet object as the - // qid join. A leaf whose FIRST object (author=carol) is a decoy and whose qid - // (qt-99) lives in a LATER object must revert — even when the backend signs the - // decoy author — so a mis-ordered includes.tweets can't credit a non-author. - function test_webTransfer_recipientLess_quote_authorInDifferentObject_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-obj"; - string memory requestPath = "/2/tweets?ids=tweet-quote-obj"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - // First object (decoy, carol) has a different id; qt-99 lives in the SECOND. - bytes memory quotedAuthor = abi.encodePacked( - '"tweets":[{"id":"other","author_id":"', - _idFor("carol"), - '"},{"id":"qt-99","author_id":"', - _idFor("bob"), - '"' - ); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - // Backend signs carol (the decoy first-object author) as receiver. - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("carol"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(QuotedAuthorDifferentObject.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "carol", - _idFor("carol"), - "TST", - "400", - 400e18, - "" - ); - } - - // The proven quoted author (carol) must equal the backend receiverUserId (bob) — - // a swapped author id reverts even with a valid, joined leaf. - function test_webTransfer_recipientLess_quote_forgedAuthor_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-2"; - string memory requestPath = "/2/tweets?ids=tweet-quote-2"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - // Leaf proves carol authored the quoted tweet... - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("carol"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - // ...but the backend signs bob as the receiver id → mismatch. - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(QuotedAuthorMismatch.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // Wrong-object hijack: the author leaf carries bob's author_id but for a DIFFERENT - // tweet id than the one proven quoted (qt-99) → the join fails and reverts. - function test_webTransfer_recipientLess_quote_wrongQuotedId_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-3"; - string memory requestPath = "/2/tweets?ids=tweet-quote-3"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - // author leaf binds bob but to tweet "qt-OTHER", not the quoted qt-99. - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-OTHER","author_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(QuotedAuthorMissingQuotedId.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // A non-quoted reference (e.g. a `replied_to` entry) must not pass as a quote - // binding: the ref leaf must contain `"type":"quoted"` (order-independent). - function test_webTransfer_recipientLess_quote_notQuotedRef_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-4"; - string memory requestPath = "/2/tweets?ids=tweet-quote-4"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - // A replied_to reference, not a quoted one. - bytes memory quotedRef = abi.encodePacked('{"type":"replied_to","id":"qt-99"}'); - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(RefNotQuote.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // Leading-decoy hijack: the recv transcript is fully committed, so a proof - // generator can reveal ANY authentic substring as the author leaf — e.g. a - // `data`-object span whose FIRST author_id is the sender (or any non-quoted - // author) followed by the referenced `"id":""`. The join would pass, but - // the extracted author would be the wrong party. The `"tweets":[{` prefix check - // rejects any leaf that is not the includes.tweets array. - function test_webTransfer_recipientLess_quote_nonIncludesLeaf_reverts() public { - _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 1000e18); - - string memory resourceId = "tweet-quote-5"; - string memory requestPath = "/2/tweets?ids=tweet-quote-5"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - // A data-object-style span (NOT `"tweets":[{`): first author_id + the - // referenced quoted id. Join passes, author = bob, but it is not includes. - bytes memory quotedAuthor = - abi.encodePacked('"author_id":"', _idFor("bob"), '","referenced_tweets":[{"type":"quoted","id":"qt-99"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(QuotedAuthorLeafNotIncludes.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // Preflight parity: verifyProof accepts a valid quote binding (must not over-reject). - function test_verifyProof_recipientLess_quote_ok() public { - string memory resourceId = "tweet-quote-vp"; - string memory requestPath = "/2/tweets?ids=tweet-quote-vp"; - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka_agent honor with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - bytes memory quotedRef = abi.encodePacked('{"type":"quoted","id":"qt-99"}'); - bytes memory quotedAuthor = abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'); - - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", requestPath, body, authorBytes, authorId, quotedRef, quotedAuthor, block.timestamp - ); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.x.com", resourceType: "tweet", resourceId: resourceId, requestPath: requestPath - }); - - assertTrue( - bank.verifyProof( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - _idFor("bob") - ) - ); - } - - function _buildBackendSig( - bytes32 uid, - bytes memory revealedBody, - bytes memory revealedAuthor, - string memory receiverUserId, - uint256 ts - ) internal pure returns (BackendSig memory) { - // Mirrors Bank._verifyBackendSignature (empty revealedAuthorId adds nothing). - bytes32 digest = - keccak256(abi.encodePacked(uid, revealedBody, revealedAuthor, keccak256(bytes(receiverUserId)), ts)); - return BackendSig({sig: _sign(BACKEND_KEY, digest), timestamp: ts}); - } - - // ═══════════════════════════════════════════════════════════════ - // TESTS - // ═══════════════════════════════════════════════════════════════ - - // 1. Alice registers and her wallet is discoverable - function test_registeredUserIsDiscoverable() public { - WebWallet alice = _alice(); - - assertTrue(address(alice) != address(0)); - assertEq(registry.resolve("api.x.com", "alice"), address(alice)); - - (string[] memory platforms, string[] memory handles) = registry.getHandles(address(alice)); - assertEq(platforms.length, 1); - assertEq(platforms[0], "api.x.com"); - assertEq(handles[0], "alice"); - } - - // 2. Depositing to registered user lands in registered balance - function test_depositToRegisteredUser() public { - WebWallet alice = _alice(); - - _fund("x", "alice", 500e18); - - _assertBankBalance(alice, 500e18); - _assertBankBalance("x", "alice", 500e18); - } - - // 3. Depositing to unregistered user lands in unregistered balance - function test_depositToUnregisteredUser() public { - _fund("x", "charlie", 200e18); - - _assertUnregisteredBalance("x", "charlie", 200e18); - _assertBankBalance("x", "charlie", 200e18); - } - - // 4. Unregistered balance migrates when user registers and does a write - function test_unregisteredBalanceMigratesOnWrite() public { - _fund("x", "charlie", 300e18); - - WebWallet charlie = _charlie(); - _assertBankBalance(charlie, 300e18); - _assertUnregisteredBalance("x", "charlie", 300e18); - _assertRegisteredBalance(charlie, 0); - - _fund("x", "charlie", 10e18); - - _assertRegisteredBalance(charlie, 310e18); - _assertUnregisteredBalance("x", "charlie", 0); - } - - // 5. Alice sends TST to registered Bob via web transfer - function test_webTransferRegisteredToRegistered() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - _transfer("github", "alice", "github", "bob", 400e18); - - _assertBankBalance("github", "alice", 600e18); - _assertBankBalance("github", "bob", 400e18); - } - - // 6. Alice sends TST to unregistered Charlie; Charlie registers and sees balance - function test_webTransferToUnregisteredThenRegister() public { - _walletFor("github", "alice", ALICE_PK); - _fund("github", "alice", 500e18); - - _transfer("github", "alice", "github", "charlie", 150e18); - - _assertUnregisteredBalance("github", "charlie", 150e18); - - WebWallet charlie = _walletFor("github", "charlie", CHARLIE_PK); - _assertBankBalance(charlie, 150e18); - } - - // 6b. webTransfer with unregistered token by address (address-based overload) - function test_webTransferByTokenAddress_unregistered_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - - // Deploy a new token that is NOT registered in Bank - MockERC20 unregToken = new MockERC20("Unregistered", "UNREG"); - unregToken.mint(address(this), 10_000e18); - - // Fund alice's wallet directly and approve bank - WebWallet aliceWallet = WebWallet(payable(registry.resolve("api.github.com", "alice"))); - require(unregToken.transfer(address(aliceWallet), 1000e18), "transfer failed"); - // Alice approves Bank to pull tokens (auto top-up) - bytes memory approveData = abi.encodeCall(IERC20.approve, (address(bank), type(uint256).max)); - _execute(aliceWallet, address(unregToken), approveData, ALICE_PK); - - // Use address-based webTransfer overload - _transferCounter++; - string memory resourceId = string(abi.encodePacked("repo/", vm.toString(_transferCounter), "/comment/1")); - string memory requestPath = - string(abi.encodePacked("/repos/owner/repo/issues/comments/", vm.toString(_transferCounter))); - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - // Body must contain the lowercase hex address of the token and the human-readable amount - string memory tokenAddrHex = vm.toLowercase(vm.toString(address(unregToken))); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 ", tokenAddrHex); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - // The web-transfer surface is gated to REGISTERED tokens — the by-address - // overload must reject an unregistered ERC-20 (else autoTopUp would pull an - // arbitrary approved token into the ledger). - vm.expectRevert(TokenNotRegistered.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - address(unregToken), - "400", - 400e18, - "" - ); - } - - // 7. Same comment cannot be used for two transfers (replay protection) - function test_replayProtectionOnWebTransfer() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - _transferWithUid("replay/1", "github", "alice", "github", "bob", 100e18); - - vm.expectRevert(TransferAlreadyUsed.selector); - _transferWithUid("replay/1", "github", "alice", "github", "bob", 100e18); - } - - // 8. Alice withdraws from bank to her wallet via execute() - function test_withdrawViaExecute() public { - WebWallet alice = _alice(); - _fund("x", "alice", 500e18); - - bytes memory data = abi.encodeCall(IBank.withdraw, (address(alice), address(token), 200e18)); - _execute(alice, address(bank), data, ALICE_PK); - - _assertTokenBalance(alice, 200e18); - _assertBankBalance(alice, 300e18); - } - - // 9. Wallet approves ERC20 and deposits for Bob in one batch - function test_batchApproveAndDeposit() public { - WebWallet alice = _alice(); - _bob(); - _fundWallet(alice, 300e18); - - address[] memory targets = new address[](2); - uint256[] memory values = new uint256[](2); - bytes[] memory datas = new bytes[](2); - uint256[] memory pks = new uint256[](1); - - targets[0] = address(token); - targets[1] = address(bank); - values[0] = 0; - values[1] = 0; - datas[0] = abi.encodeCall(IERC20.approve, (address(bank), 300e18)); - datas[1] = abi.encodeCall(IBank.deposit, ("api.x.com", "bob", _idFor("bob"), address(token), 300e18)); - pks[0] = ALICE_PK; - - _executeBatch(alice, targets, values, datas, pks); - - _assertBankBalance("x", "bob", 300e18); - } - - // 10. Multisig with 3 handles from same platform (threshold=3) - function test_multisigThreeSamePlatform() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - - _linkIdentityWithSessionPk(alice, "x", "alice2", ALICE_PK, ALICE_PK2); - - _linkIdentityWithSessionPk(alice, "x", "alice3", ALICE_PK, ALICE_PK3); - - _setThreshold(alice, 3, ALICE_PK); - - // 2 sigs → fails - bytes memory data = abi.encodeCall(Registry.resolve, ("api.x.com", "alice")); - { - uint256[] memory pks2 = new uint256[](2); - pks2[0] = ALICE_PK; - pks2[1] = ALICE_PK2; - uint256 n = alice.nonce(); - bytes32 digest = - keccak256(abi.encode(address(registry), uint256(0), keccak256(data), n, block.chainid, address(alice))); - bytes[] memory sigs = new bytes[](2); - sigs[0] = _sign(pks2[0], digest); - sigs[1] = _sign(pks2[1], digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - alice.execute(address(registry), 0, data, sigs); - } - - // 3 sigs → succeeds - { - uint256[] memory pks3 = new uint256[](3); - pks3[0] = ALICE_PK; - pks3[1] = ALICE_PK2; - pks3[2] = ALICE_PK3; - _executeMulti(alice, address(registry), 0, data, pks3); - } - } - - // 11. Multisig with handles across different platforms (threshold=2) - function test_multisigCrossPlatform() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - - _linkIdentityWithSessionPk(alice, "github", "alice_gh", ALICE_PK, ALICE_PK2); - - _setThreshold(alice, 2, ALICE_PK); - - bytes memory data = abi.encodeCall(Registry.resolve, ("api.x.com", "alice")); - - // 1 sig → fails - { - uint256 n = alice.nonce(); - bytes32 digest = - keccak256(abi.encode(address(registry), uint256(0), keccak256(data), n, block.chainid, address(alice))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(ALICE_PK, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - alice.execute(address(registry), 0, data, sigs); - } - - // 2 sigs same identity → fails - { - _addSession("x", "alice", ALICE_PK3); - uint256 n = alice.nonce(); - bytes32 digest = - keccak256(abi.encode(address(registry), uint256(0), keccak256(data), n, block.chainid, address(alice))); - bytes[] memory sigs = new bytes[](2); - sigs[0] = _sign(ALICE_PK, digest); - sigs[1] = _sign(ALICE_PK3, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - alice.execute(address(registry), 0, data, sigs); - } - - // 1 x + 1 github → succeeds - { - uint256[] memory pks = new uint256[](2); - pks[0] = ALICE_PK; - pks[1] = ALICE_PK2; - _executeMulti(alice, address(registry), 0, data, pks); - } - } - - // 12. Upgrade wallet to V2, then execute transfer - function test_upgradeToV2ThenExecute() public { - WebWallet alice = _alice(); - - MockWebWalletV2 v2Impl = new MockWebWalletV2(); - factory.upgradeWalletImplementation(address(v2Impl)); - - bytes memory upgradeData = abi.encodeCall(WebWallet.upgradeToLatest, ()); - _execute(alice, address(alice), upgradeData, ALICE_PK); - - assertEq(MockWebWalletV2(payable(address(alice))).version(), 2); - - _fund("x", "alice", 500e18); - - bytes memory withdrawData = abi.encodeCall(IBank.withdraw, (address(alice), address(token), 200e18)); - _execute(alice, address(bank), withdrawData, ALICE_PK); - - _assertTokenBalance(alice, 200e18); - _assertBankBalance(alice, 300e18); - } - - // 13. Revoked session key no longer counts toward threshold - function test_revokedSessionKeyFails() public { - WebWallet alice = _alice(); - _addSession("x", "alice", ALICE_PK2); - - // Both keys work initially - bytes memory data = abi.encodeCall(Registry.resolve, ("api.x.com", "alice")); - _execute(alice, address(registry), data, ALICE_PK); - - // Revoke key1 - _revokeSession(alice, "x", "alice", vm.addr(ALICE_PK), ALICE_PK2); - - // key1 sig fails - { - uint256 n = alice.nonce(); - bytes32 digest = - keccak256(abi.encode(address(registry), uint256(0), keccak256(data), n, block.chainid, address(alice))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(ALICE_PK, digest); - vm.expectRevert(WebWallet.ThresholdNotMet.selector); - alice.execute(address(registry), 0, data, sigs); - } - - // key2 succeeds - _execute(alice, address(registry), data, ALICE_PK2); - } - - // 14. linkIdentity adds new platform, balance aggregates across platforms - function test_linkIdentityAggregatesBalance() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - - _fund("github", "alice_gh", 250e18); - - _linkIdentityWithSessionPk(alice, "github", "alice_gh", ALICE_PK, LINK_PK1); - - _assertBankBalance(alice, 250e18); - - _fund("x", "alice", 100e18); - _assertBankBalance(alice, 350e18); - } - - // 15. Upgrade then verify state preserved - function test_upgradePreservesState() public { - WebWallet alice = _alice(); - _fund("x", "alice", 500e18); - _linkIdentityWithSessionPk(alice, "github", "alice_gh", ALICE_PK, LINK_PK1); - _setThreshold(alice, 2, ALICE_PK); - - // Upgrade - MockWebWalletV2 v2Impl = new MockWebWalletV2(); - factory.upgradeWalletImplementation(address(v2Impl)); - { - uint256[] memory pks = new uint256[](2); - pks[0] = ALICE_PK; - pks[1] = LINK_PK1; - bytes memory upgradeData = abi.encodeCall(WebWallet.upgradeToLatest, ()); - _executeMulti(alice, address(alice), 0, upgradeData, pks); - } - - // Verify state preserved - assertEq(MockWebWalletV2(payable(address(alice))).version(), 2); - assertEq(alice.threshold(), 2); - assertEq(alice.identityCount(), 2); - assertTrue(alice.isSession(vm.addr(ALICE_PK))); - assertTrue(alice.isSession(vm.addr(LINK_PK1))); - _assertBankBalance(alice, 500e18); - } - - // 16. executeBatch: approve + deposit for another user atomically - function test_batchApproveAndDepositForSelf() public { - WebWallet alice = _alice(); - _fundWallet(alice, 1000e18); - - address[] memory targets = new address[](2); - uint256[] memory values = new uint256[](2); - bytes[] memory datas = new bytes[](2); - uint256[] memory pks = new uint256[](1); - - targets[0] = address(token); - targets[1] = address(bank); - values[0] = 0; - values[1] = 0; - datas[0] = abi.encodeCall(IERC20.approve, (address(bank), 500e18)); - datas[1] = abi.encodeCall(IBank.deposit, ("api.x.com", "alice", _idFor("alice"), address(token), 500e18)); - pks[0] = ALICE_PK; - - _executeBatch(alice, targets, values, datas, pks); - - _assertBankBalance(alice, 500e18); - _assertTokenBalance(alice, 500e18); - } - - // 17. linkIdentity registers session on wallet (cross-contract) - function test_linkIdentityRegistersSession() public { - WebWallet alice = _alice(); - _linkIdentityWithSessionPk(alice, "github", "alice_gh", ALICE_PK, LINK_PK1); - - // Session is keyed by the immutable (platform, userId), not the handle. - bytes32 ghKey = keccak256(abi.encode("api.github.com", _idFor("alice_gh"))); - assertTrue(alice.isSession(vm.addr(LINK_PK1))); - assertEq(alice.sessionCount(ghKey), 1); - assertEq(alice.sessionAt(ghKey, 0), vm.addr(LINK_PK1)); - } - - // 18. webTransfer sender not registered - function test_webTransfer_senderNotRegistered_reverts() public { - _walletFor("github", "bob", BOB_PK); - _fund("github", "bob", 100e18); - // alice is not registered on github - vm.expectRevert(SenderNotRegistered.selector); - _transfer("github", "alice_unregistered", "github", "bob", 50e18); - } - - // 19. webTransfer insufficient balance - function test_webTransfer_insufficientBalance_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 100e18); - vm.expectRevert(); // _autoTopUp tries safeTransferFrom without allowance - _transfer("github", "alice", "github", "bob", 200e18); - } - - // 20. webTransfer zero amount - function test_webTransfer_zeroAmount_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 100e18); - vm.expectRevert(ZeroAmount.selector); - _transferWithAmountStr("github", "alice", "github", "bob", "0", 0); - } - - // ─── parseDecimalAmount tests ─────────────────────────────────── - - // 21. Fractional transfer with 0.05 (18 decimals) - function test_webTransfer_fractionalAmount() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1e18); - - _transferWithAmountStr("github", "alice", "github", "bob", "0.05", 5e16); - - _assertBankBalance("github", "alice", 1e18 - 5e16); - _assertBankBalance("github", "bob", 5e16); - } - - // 22. Fractional transfer with 1.5 (18 decimals) - function test_webTransfer_decimalOnePointFive() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 10e18); - - _transferWithAmountStr("github", "alice", "github", "bob", "1.5", 1.5e18); - - _assertBankBalance("github", "alice", 10e18 - 1.5e18); - _assertBankBalance("github", "bob", 1.5e18); - } - - // 23. Integer amount still works with amountStr - function test_webTransfer_integerAmountStr() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - _transferWithAmountStr("github", "alice", "github", "bob", "100", 100e18); - - _assertBankBalance("github", "alice", 900e18); - _assertBankBalance("github", "bob", 100e18); - } - - // 24. Amount wei mismatch reverts - function test_webTransfer_amountWeiMismatch_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - vm.expectRevert(AmountWeiMismatch.selector); - _transferWithAmountStr("github", "alice", "github", "bob", "100", 99e18); - } - - // ─── Token registration tests ─────────────────────────────────── - - /// Unregistered token name must not silently resolve to native asset. - function test_webTransfer_unregisteredTokenName_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 100e18); - - // Build a transfer that uses "$FAKE" as token name — not registered. - _transferCounter++; - string memory resourceId = string(abi.encodePacked("repo/", vm.toString(_transferCounter), "/comment/1")); - string memory requestPath = - string(abi.encodePacked("/repos/owner/repo/issues/comments/", vm.toString(_transferCounter))); - - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 50 $FAKE"); - bytes memory authorBytes = bytes("alice"); - - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - NotaryTlsProof memory notaryProof = - _buildBankProof("api.github.com", requestPath, body, authorBytes, block.timestamp); - BackendSig memory backendSig = _buildBackendSig(uid, body, authorBytes, "", block.timestamp); - - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(TokenNotRegistered.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: ""}), - body, - notaryProof, - backendSig, - "bob", - "", - "$FAKE", - "50", - 50e18, - "" - ); - } - - // ═══════════════════════════════════════════════════════════════ - // MUTATION-GUARD NEGATIVE TESTS - // One per revert-guard the mutation sweep found uncaught. Each feeds - // the bad input and asserts the exact custom error fires. - // ═══════════════════════════════════════════════════════════════ - - /// @dev Recompute the notary digest LibProof signs (chainid + registry bound). - function _notaryDigest(NotaryTlsProof memory p) internal view returns (bytes32) { - return keccak256( - abi.encode( - block.chainid, - address(registry), - p.domainHash, - p.clientRandom, - p.serverRandom, - keccak256(p.serverEphemeralKey), - p.transcriptRoot, - p.timestamp - ) - ); - } - - /// @dev A fully valid id-only github honor proof + backend sig + resource, for - /// verifyProof-direct guard tests that then tamper ONE field. - function _validGithubProof(string memory resourceType, string memory requestPath, bytes memory body) - internal - view - returns (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) - { - string memory resourceId = "repo/guard/comment/1"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", resourceType, ":", resourceId)); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - notaryProof = _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - backendSig = _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - resource = ResourceInfo({ - platform: "api.github.com", resourceType: resourceType, resourceId: resourceId, requestPath: requestPath - }); - } - - // 1. Unknown resource type (no seeded prefix) → UnknownResourceType. - function test_UnknownResourceType_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("bogus", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(UnknownResourceType.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 2. Valid prefix ("issue_comment" → "/repos/") but the request path lacks it. - // The endpoint leaf is built over the SAME wrong path so it still verifies; - // the prefix check (before sigs) fires first → RequestPathMismatch. - function test_RequestPathMismatch_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/wrong/path", body); - vm.expectRevert(RequestPathMismatch.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 3. sender.author is not a substring of revealedAuthor → AuthorNotInRevealedField - // (the author-id leaf is valid, so the check that fires is this one). - function test_AuthorNotInRevealedField_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(AuthorNotInRevealedField.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "notinside", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 4. verifyProof with the platform's templates cleared → TemplateNotFound (the - // empty-templates guard at the verifyProof level). - function test_TemplateNotFound_emptyTemplates_reverts() public { - bank.clearPlatformTemplates("api.github.com"); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(TemplateNotFound.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 5. A valid proof over an unrelated body that matches no template → matchAny - // returns false → TemplateNotFound. - function test_TemplateNotFound_noMatch_reverts() public { - bytes memory body = abi.encodePacked("totally unrelated text"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(TemplateNotFound.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 6. Notary digest signed by a non-notary key → InvalidNotarySignature. - function test_InvalidNotarySignature_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - notaryProof.notarySig = _sign(uint256(0xBAD), _notaryDigest(notaryProof)); - vm.expectRevert(InvalidNotarySignature.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 7. proof.domainHash points at a different domain (and the notary signs THAT), - // but the domain leaf is over the real platform → DomainHashMismatch. - function test_DomainHashMismatch_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - notaryProof.domainHash = keccak256("wrong.domain"); - notaryProof.notarySig = _sign(NOTARY_KEY, _notaryDigest(notaryProof)); - vm.expectRevert(DomainHashMismatch.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 8. An over-long merkle path (33 > 32) on the first-verified (body) leaf → - // MerklePathTooLong (before the Merkle check). - function test_MerklePathTooLong_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - notaryProof.bodyMerklePath = new bytes32[](33); - vm.expectRevert(MerklePathTooLong.selector); - bank.verifyProof( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - _idFor("bob") - ); - } - - // 9. _executeTransfer: sender's id was never registered (receiver is) → the - // sender resolveById returns 0 → SenderNotRegistered. - function test_SenderNotRegistered_executeTransfer_reverts() public { - _walletFor("github", "bob", BOB_PK); // receiver registered - _fund("github", "bob", 1000e18); - - string memory resourceId = "repo/snr/comment/1"; - string memory requestPath = "/repos/owner/repo/issues/comments/1"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 TST"); - bytes memory authorBytes = bytes("ghost"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("ghost"), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(SenderNotRegistered.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "ghost", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // 11. The by-ADDRESS webTransferV2 overload with token == address(0) → ZeroToken. - function test_ZeroToken_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(ZeroToken.selector); - bank.webTransferV2( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - address(0), - "5", - 5e18, - "" - ); - } - - // 12. sourceUrl present but not https → SourceUrlNotHttps. - function test_SourceUrlNotHttps_reverts() public { - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(SourceUrlNotHttps.selector); - bank.webTransferV2( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "5", - 5e18, - "http://github.com/owner/repo" - ); - } - - // 13. sourceUrl https but not under the platform web prefix → SourceUrlDomainMismatch. - // The underlying transfer is otherwise valid (removing the guard settles it). - function test_SourceUrlDomainMismatch_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - _fund("github", "alice", 1000e18); - - string memory resourceId = "repo/srcurl/comment/1"; - string memory requestPath = "/repos/owner/repo/issues/comments/1"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 400 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(SourceUrlDomainMismatch.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "https://evil.com/owner/repo" - ); - } - - // ── Recipient-less structural guards (LibRecipient) ────────────────── - // Reached end-to-end: a valid recipient-less X body makes verifyTemplate flag - // recipientLess, so webTransferV2 calls verifyRecipientIdLeaf with the tampered - // reply/quote field BEFORE the rest of proof verification. - - ResourceInfo internal _rlResource = ResourceInfo({ - platform: "api.x.com", - resourceType: "tweet", - resourceId: "tweet-rl-guard", - requestPath: "/2/tweets?ids=tweet-rl-guard" - }); - - function _rlBody() internal pure returns (bytes memory) { - return abi.encodePacked("@dyaka_agent honor with 400 TST"); - } - - function _rlSender() internal view returns (SenderInfo memory) { - return SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }); - } - - function _rlBackendSig() internal view returns (BackendSig memory) { - bytes32 uid = keccak256(abi.encodePacked("api.x.com", ":", "tweet", ":", "tweet-rl-guard")); - return _buildBackendSigWithId( - uid, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - _idFor("bob"), - block.timestamp - ); - } - - function _rlSubmit(NotaryTlsProof memory notaryProof) internal { - bank.webTransferV2( - _rlResource, - _rlSender(), - _rlBody(), - notaryProof, - _rlBackendSig(), - "bob", - _idFor("bob"), - "TST", - "400", - 400e18, - "" - ); - } - - // 20. revealedReceiverId shorter than the "in_reply_to_user_id": key. - function test_ReplyLeafTooShort_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('"in_reply"'), - block.timestamp - ); - vm.expectRevert(ReplyLeafTooShort.selector); - _rlSubmit(notaryProof); - } - - // 21. revealedReceiverId long enough but not the in_reply_to_user_id key. - function test_ReplyLeafNotInReplyTo_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('"in_reply_to_user_XX":"9"'), - block.timestamp - ); - vm.expectRevert(ReplyLeafNotInReplyTo.selector); - _rlSubmit(notaryProof); - } - - // 28. reply target value is empty ("") → EmptyQuotedValue (via _readQuotedValue). - function test_EmptyQuotedValue_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithReceiverId( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('"in_reply_to_user_id":""'), - block.timestamp - ); - vm.expectRevert(EmptyQuotedValue.selector); - _rlSubmit(notaryProof); - } - - // 22. quoted ref shorter than 2 bytes → QuotedRefTooShort. - function test_QuotedRefTooShort_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes("{"), - abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'), - block.timestamp - ); - vm.expectRevert(QuotedRefTooShort.selector); - _rlSubmit(notaryProof); - } - - // 23. quoted ref not a {...} object → QuotedRefNotObject. - function test_QuotedRefNotObject_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes("XY"), - abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'), - block.timestamp - ); - vm.expectRevert(QuotedRefNotObject.selector); - _rlSubmit(notaryProof); - } - - // 24. quoted ref with a nested { } inside → QuotedRefNotFlatObject. - function test_QuotedRefNotFlatObject_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes("{{}}"), - abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'), - block.timestamp - ); - vm.expectRevert(QuotedRefNotFlatObject.selector); - _rlSubmit(notaryProof); - } - - // 25. flat quoted ref with type:quoted but no "id" → QuotedRefMissingId. - function test_QuotedRefMissingId_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('{"type":"quoted"}'), - abi.encodePacked('"tweets":[{"id":"qt-99","author_id":"', _idFor("bob"), '"'), - block.timestamp - ); - vm.expectRevert(QuotedRefMissingId.selector); - _rlSubmit(notaryProof); - } - - // 26. includes-tweets leaf shorter than the "tweets":[{ key → QuotedAuthorLeafTooShort. - function test_QuotedAuthorLeafTooShort_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('{"type":"quoted","id":"qt-99"}'), - bytes('"tweets"'), - block.timestamp - ); - vm.expectRevert(QuotedAuthorLeafTooShort.selector); - _rlSubmit(notaryProof); - } - - // 27. includes-tweets leaf has the qid join but no author_id → QuotedAuthorMissingAuthorId. - function test_QuotedAuthorMissingAuthorId_reverts() public { - NotaryTlsProof memory notaryProof = _buildBankProofWithQuotedAuthor( - "api.x.com", - _rlResource.requestPath, - _rlBody(), - bytes("alice"), - abi.encodePacked('"author_id":"', _idFor("alice"), '"'), - bytes('{"type":"quoted","id":"qt-99"}'), - bytes('"tweets":[{"id":"qt-99"'), - block.timestamp - ); - vm.expectRevert(QuotedAuthorMissingAuthorId.selector); - _rlSubmit(notaryProof); - } - - // 29. amountStr with two decimal points → MultipleDecimalPoints (parseDecimalAmount). - // The explicit github template matches "1.2.3" as the amount, then _webTransfer - // parses it. amount (999e18) is deliberately non-matching so removing the guard - // yields AmountWeiMismatch, not this error. - function test_MultipleDecimalPoints_reverts() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - - string memory resourceId = "repo/dec/comment/1"; - string memory requestPath = "/repos/owner/repo/issues/comments/1"; - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 1.2.3 TST"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - vm.expectRevert(LibString.MultipleDecimalPoints.selector); - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "1.2.3", - 999e18, - "" - ); - } - - // 31. by-name webTransferV2 for a platform with NO templates → LibTemplate - // .verifyTemplate hits count == 0 → TemplateNotFound (distinct from the - // verifyProof-level guard; removing it yields NoTemplateMatch instead). - function test_TemplateNotFound_verifyTemplateEmpty_reverts() public { - bank.clearPlatformTemplates("api.github.com"); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 5 TST"); - (NotaryTlsProof memory notaryProof, BackendSig memory backendSig, ResourceInfo memory resource) = - _validGithubProof("issue_comment", "/repos/owner/repo/issues/comments/1", body); - vm.expectRevert(TemplateNotFound.selector); - bank.webTransferV2( - resource, - SenderInfo({ - author: "alice", - revealedAuthor: bytes("alice"), - revealedAuthorId: abi.encodePacked('"author_id":"', _idFor("alice"), '"') - }), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "TST", - "5", - 5e18, - "" - ); - } -} diff --git a/solidity/archived/transfer/test/IntegrationNewFeatures.t.sol b/solidity/archived/transfer/test/IntegrationNewFeatures.t.sol deleted file mode 100644 index c9431137..00000000 --- a/solidity/archived/transfer/test/IntegrationNewFeatures.t.sol +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IntegrationTest} from "./Integration.t.sol"; -import {IBank} from "../bank/IBank.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -/// @title Integration tests for transfer_within, balanceOf, balanceOfTotal -/// @dev Extends the existing IntegrationTest with new story chapters. -contract IntegrationNewFeaturesTest is IntegrationTest { - // ═══════════════════════════════════════════════════════════════ - // Helper: transfer_within via execute() - // ═══════════════════════════════════════════════════════════════ - - function _transferWithin( - WebWallet sender, - uint256 senderPk, - string memory receiverPlatform, - string memory receiverHandle, - uint256 amount - ) internal { - bytes memory data = abi.encodeCall( - IBank.transfer_within, - (_domain(receiverPlatform), receiverHandle, _idFor(receiverHandle), address(token), amount) - ); - _execute(sender, address(bank), data, senderPk); - } - - function _approveBank(WebWallet wallet, uint256 amount, uint256 pk) internal { - bytes memory data = abi.encodeCall(IERC20.approve, (address(bank), amount)); - _execute(wallet, address(token), data, pk); - } - - // ═══════════════════════════════════════════════════════════════ - // CHAPTER: transfer_within flow - // ═══════════════════════════════════════════════════════════════ - - /// @notice Full transfer_within story: registered→registered, registered→unregistered, then late registration - function test_story_transferWithinFlow() public { - // 1. Alice registers via OAuth, deposits 500 USDC into Bank - WebWallet alice = _alice(); - _fund("x", "alice", 500e18); - _assertBankBalance(alice, 500e18); - - // 2. Bob registers via OAuth - WebWallet bob = _bob(); - - // 3. Alice calls transfer_within("x", "bob", usdc, 200) via execute() - _transferWithin(alice, ALICE_PK, "x", "bob", 200e18); - - // 4. Assert Alice's balanceOf = 300, Bob's balanceOf = 200 - _assertBankBalance("x", "alice", 300e18); - _assertBankBalance("x", "bob", 200e18); - _assertBankBalance(alice, 300e18); - _assertBankBalance(bob, 200e18); - - // 5. Alice calls transfer_within to an unregistered handle "charlie" - _transferWithin(alice, ALICE_PK, "x", "charlie", 100e18); - - // 6. Assert balanceOf("x", "charlie", usdc) = 100 in unregistered slot - _assertBankBalance("x", "charlie", 100e18); - _assertUnregisteredBalance("x", "charlie", 100e18); - _assertBankBalance(alice, 200e18); - - // 7. Charlie later registers — balanceOf reflects the migrated balance - WebWallet charlie = _charlie(); - _assertBankBalance(charlie, 100e18); - _assertBankBalance("x", "charlie", 100e18); - } - - // ═══════════════════════════════════════════════════════════════ - // CHAPTER: balanceOf / balanceOfTotal - // ═══════════════════════════════════════════════════════════════ - - /// @notice Full balance view story: zero → deposit → wallet ERC20 → cross-check variants - function test_story_balanceViews() public { - // 1. Fresh Alice wallet with 0 balance - WebWallet alice = _alice(); - assertEq(bank.balanceOf(address(alice), address(token)), 0); - assertEq(bank.balanceOfTotal(address(alice), address(token)), 0); - - // 2. Alice deposits 300 USDC into Bank - _fund("x", "alice", 300e18); - assertEq(bank.balanceOf(address(alice), address(token)), 300e18); - assertEq(bank.balanceOfTotal(address(alice), address(token)), 300e18); - - // 3. Alice also holds 100 USDC directly in her WebWallet (ERC20 transfer) - _fundWallet(alice, 100e18); - assertEq(bank.balanceOf(address(alice), address(token)), 300e18); // Bank-only unchanged - assertEq(bank.balanceOfTotal(address(alice), address(token)), 400e18); // Bank + wallet ERC20 - - // 4. Test both address and (platform, handle) variants return same values - assertEq( - bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(token)), - bank.balanceOf(address(alice), address(token)) - ); - assertEq( - bank.balanceOfTotal("api.x.com", "alice", _idFor("alice"), address(token)), - bank.balanceOfTotal(address(alice), address(token)) - ); - } - - // ═══════════════════════════════════════════════════════════════ - // CHAPTER: transfer_within auto-top-up - // ═══════════════════════════════════════════════════════════════ - - /// @notice Auto-top-up story: 0 Bank balance, 500 in wallet, transfer pulls from wallet - function test_story_transferWithinAutoTopUp() public { - // Setup: Alice and Bob registered - WebWallet alice = _alice(); - WebWallet bob = _bob(); - - // 1. Alice has 0 Bank balance but 500 USDC in WebWallet - _fundWallet(alice, 500e18); - _assertBankBalance(alice, 0); - _assertTokenBalance(alice, 500e18); - - // Alice approves Bank to pull from her wallet - _approveBank(alice, type(uint256).max, ALICE_PK); - - // 2. Alice calls transfer_within for 200 USDC - _transferWithin(alice, ALICE_PK, "x", "bob", 200e18); - - // 3. Assert Bank pulled 200 from WebWallet, credited Bob - _assertBankBalance(alice, 0); // All pulled, all sent - _assertTokenBalance(alice, 300e18); // 500 - 200 shortfall pulled - _assertBankBalance(bob, 200e18); // Bob received 200 - } -} diff --git a/solidity/archived/transfer/test/LibStringGuards.t.sol b/solidity/archived/transfer/test/LibStringGuards.t.sol deleted file mode 100644 index 47b600bb..00000000 --- a/solidity/archived/transfer/test/LibStringGuards.t.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {LibString} from "../bank/libraries/LibString.sol"; - -/// @dev Test-only harness exposing LibString.substringCalldata. LibTemplate only -/// ever calls it with in-bounds ranges, so its InvalidRange guard is -/// unreachable end-to-end — this isolates it. -contract LibStringHarness { - function substr(bytes calldata data, uint256 start, uint256 end) external pure returns (string memory) { - return LibString.substringCalldata(data, start, end); - } -} - -contract LibStringGuardsTest is Test { - LibStringHarness h; - - function setUp() public { - h = new LibStringHarness(); - } - - // 30. substringCalldata with end > data.length → InvalidRange (removing the - // guard yields an out-of-bounds slice panic instead). - function test_InvalidRange_reverts() public { - bytes memory data = bytes("hello"); - vm.expectRevert(LibString.InvalidRange.selector); - h.substr(data, 0, data.length + 1); - } -} diff --git a/solidity/archived/transfer/test/NativeAsset.t.sol b/solidity/archived/transfer/test/NativeAsset.t.sol deleted file mode 100644 index f313e4cc..00000000 --- a/solidity/archived/transfer/test/NativeAsset.t.sol +++ /dev/null @@ -1,460 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../login/Registry.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "../bank/BankTypes.sol"; -import {NativeAmountMismatch, NativeTransferFailed, AlreadyRegistered} from "../bank/BankErrors.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {WalletFactory} from "../../login/WalletFactory.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; -import {MockERC20} from "../MockERC20.sol"; - -/// @dev Refuses incoming ETH so a native withdraw's low-level call fails. -contract RejectEther { - receive() external payable { - revert("no ether"); - } -} - -contract NativeAssetTest is Test, BankDiamondDeployer { - uint256 constant NOTARY_KEY = uint256(keccak256("notary")); - uint256 constant BACKEND_KEY = uint256(keccak256("backend")); - address NOTARY; - address BACKEND; - - Registry registry; - IBank bank; - WalletFactory factory; - - uint256 constant ALICE_PK = 0xA001; - uint256 constant BOB_PK = 0xB001; - uint256 private _transferCounter; - - function setUp() public { - NOTARY = vm.addr(NOTARY_KEY); - BACKEND = vm.addr(BACKEND_KEY); - - WebWallet walletImpl = new WebWallet(); - Registry rImpl = new Registry(); - WalletFactory fImpl = new WalletFactory(); - - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(walletImpl), address(1))) - ) - ) - ); - Notary notaryReg = deployNotary(address(this), NOTARY); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall(Registry.initialize, (address(notaryReg), BACKEND, address(factory), address(this))) - ) - ) - ); - factory.setRegistry(address(registry)); - bank = IBank(deployBankDiamond(address(this), address(notaryReg), BACKEND, address(registry))); - - // Register native asset - bank.registerToken("ETH", address(0)); - - // Register platforms used in tests (X-style quoted id binding). - registry.setPlatform("api.x.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - registry.setPlatform("api.github.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - - vm.warp(1_000_000); - vm.deal(address(this), 1000 ether); - } - - /// Deterministic immutable id for a handle (escrow keyed by id). - function _idFor(string memory handle) internal pure returns (string memory) { - return string(abi.encodePacked("id_", handle)); - } - - // ─── Helpers ───────────────────────────────────────────────── - - function _domain(string memory platform) internal pure returns (string memory) { - if (keccak256(bytes(platform)) == keccak256("x")) return "api.x.com"; - if (keccak256(bytes(platform)) == keccak256("github")) return "api.github.com"; - return platform; - } - - function _walletFor(string memory platform, string memory handle, uint256 pk) internal returns (WebWallet) { - string memory domain = _domain(platform); - string memory userId = _idFor(handle); - address sessionAddr = vm.addr(pk); - Registry.FullTlsProof memory proof = _buildRegistryProof( - domain, handle, userId, "api.test.com/endpoint", '"username":"', sessionAddr, block.timestamp - ); - registry.register_session(proof, domain, handle, userId, "api.test.com/endpoint"); - return WebWallet(payable(registry.resolve(domain, handle))); - } - - function _alice() internal returns (WebWallet) { - return _walletFor("x", "alice", ALICE_PK); - } - - function _bob() internal returns (WebWallet) { - return _walletFor("x", "bob", BOB_PK); - } - - function _execute(WebWallet wallet, address target, uint256 value, bytes memory data, uint256 pk) internal { - uint256 currentNonce = wallet.nonce(); - bytes32 digest = - keccak256(abi.encode(target, value, keccak256(data), currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(pk, digest); - wallet.execute(target, value, data, sigs); - } - - function _execute(WebWallet wallet, address target, bytes memory data, uint256 pk) internal { - _execute(wallet, target, 0, data, pk); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _h(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - // 4-leaf tree (D, U, E, Id) — id bound via the quoted "id":"" recv leaf. - function _buildRegistryProof( - string memory domain, - string memory username, - string memory userId, - string memory endpoint, - string memory handlePrefix, - address userAddress, - uint256 ts - ) internal view returns (Registry.FullTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes32 leafD = _leaf("domain:", bytes(domain)); - bytes32 leafU = _leaf("recv:", abi.encodePacked(handlePrefix, username, '"')); - bytes32 leafE = _leaf("endpoint:", bytes(endpoint)); - bytes32 leafId = _leaf("recv:", abi.encodePacked('"id":"', userId, '"')); - - bytes32 hDU = _h(leafD, leafU); - bytes32 hEId = _h(leafE, leafId); - bytes32 transcriptRoot = _h(hDU, hEId); - - bytes32[] memory dp = new bytes32[](2); - dp[0] = leafU; - dp[1] = hEId; - bytes32[] memory up = new bytes32[](2); - up[0] = leafD; - up[1] = hEId; - bytes32[] memory ep = new bytes32[](2); - ep[0] = leafId; - ep[1] = hDU; - bytes32[] memory idp = new bytes32[](2); - idp[0] = leafE; - idp[1] = hDU; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(userAddress, address(0), transcriptRoot, ts)); - - proof = Registry.FullTlsProof({ - notarySignature: _sign(NOTARY_KEY, notaryDigest), - backendSignature: _sign(BACKEND_KEY, backendDigest), - userAddress: userAddress, - walletAddress: address(0), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: idp - }); - } - - /// 5-leaf proof builder (adds the author-id recv leaf) — id-only Bank always - /// verifies the author-id Merkle leaf. Ported verbatim from Integration.t.sol - /// (`_h` is the sorted-pair hash, same as Integration's `_hp`). - function _buildBankProofWithId( - string memory apiHost, - string memory requestPath, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - uint256 ts - ) internal view returns (NotaryTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(333)); - bytes32 serverRandom = bytes32(uint256(444)); - bytes memory serverEphKey = hex"cafebabe"; - bytes32 domainHash = keccak256(bytes(apiHost)); - - bytes32 leafD = _leaf("domain:", bytes(apiHost)); - bytes32 leafE = _leaf("endpoint:", bytes(requestPath)); - bytes32 leafB = _leaf("recv:", revealedBody); - bytes32 leafA = _leaf("recv:", revealedAuthor); - bytes32 leafId = _leaf("recv:", revealedAuthorId); - - bytes32 hDE = _h(leafD, leafE); - bytes32 hBA = _h(leafB, leafA); - bytes32 root4 = _h(hDE, hBA); - bytes32 transcriptRoot = _h(root4, leafId); - - bytes32[] memory bodyPath = new bytes32[](3); - (bodyPath[0], bodyPath[1], bodyPath[2]) = (leafA, hDE, leafId); - bytes32[] memory authorPath = new bytes32[](3); - (authorPath[0], authorPath[1], authorPath[2]) = (leafB, hDE, leafId); - bytes32[] memory domainPath = new bytes32[](3); - (domainPath[0], domainPath[1], domainPath[2]) = (leafE, hBA, leafId); - bytes32[] memory endpointPath = new bytes32[](3); - (endpointPath[0], endpointPath[1], endpointPath[2]) = (leafD, hBA, leafId); - bytes32[] memory authorIdPath = new bytes32[](1); - authorIdPath[0] = root4; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - - proof = NotaryTlsProof({ - notarySig: _sign(NOTARY_KEY, notaryDigest), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - bodyMerklePath: bodyPath, - authorMerklePath: authorPath, - domainMerklePath: domainPath, - endpointMerklePath: endpointPath, - authorIdMerklePath: authorIdPath, - receiverIdMerklePath: new bytes32[](0), - revealedReceiverId: "", - quotedRefMerklePath: new bytes32[](0), - revealedQuotedRef: "", - quotedAuthorMerklePath: new bytes32[](0), - revealedQuotedAuthorId: "" - }); - } - - function _buildBackendSigWithId( - bytes32 uid, - bytes memory revealedBody, - bytes memory revealedAuthor, - bytes memory revealedAuthorId, - string memory receiverUserId, - uint256 ts - ) internal pure returns (BackendSig memory) { - bytes32 digest = keccak256( - abi.encodePacked(uid, revealedBody, revealedAuthor, revealedAuthorId, keccak256(bytes(receiverUserId)), ts) - ); - return BackendSig({sig: _sign(BACKEND_KEY, digest), timestamp: ts}); - } - - // ─── Tests ─────────────────────────────────────────────────── - - function test_registerNativeToken() public view { - // Native ETH registered in setUp — assert the reverse map records it. - assertEq(bank.tokenName(address(0)), "ETH"); - } - - function test_depositNativeToRegisteredUser() public { - WebWallet alice = _alice(); - bank.deposit{value: 1 ether}("api.x.com", "alice", _idFor("alice"), address(0), 1 ether); - assertEq(bank.balanceOf(address(alice), address(0)), 1 ether); - } - - function test_depositNativeToUnregisteredUser() public { - bank.deposit{value: 2 ether}("api.x.com", "unknown", _idFor("unknown"), address(0), 2 ether); - bytes32 key = keccak256(abi.encode("id:v1", "api.x.com", _idFor("unknown"))); - assertEq(bank.unregisteredBalances(key, address(0)), 2 ether); - } - - function test_withdrawNativeViaExecute() public { - WebWallet alice = _alice(); - bank.deposit{value: 5 ether}("api.x.com", "alice", _idFor("alice"), address(0), 5 ether); - - uint256 balBefore = address(alice).balance; - bytes memory data = abi.encodeCall(IBank.withdraw, (address(alice), address(0), 3 ether)); - _execute(alice, address(bank), data, ALICE_PK); - assertEq(address(alice).balance, balBefore + 3 ether); - assertEq(bank.balanceOf(address(alice), address(0)), 2 ether); - } - - function test_transferWithinNative() public { - WebWallet alice = _alice(); - WebWallet bob = _bob(); - bank.deposit{value: 10 ether}("api.x.com", "alice", _idFor("alice"), address(0), 10 ether); - - bytes memory data = - abi.encodeCall(IBank.transfer_within, ("api.x.com", "bob", _idFor("bob"), address(0), 4 ether)); - _execute(alice, address(bank), data, ALICE_PK); - - assertEq(bank.balanceOf(address(alice), address(0)), 6 ether); - assertEq(bank.balanceOf(address(bob), address(0)), 4 ether); - } - - function test_transferWithinNative_insufficientBalance_reverts() public { - WebWallet alice = _alice(); - _bob(); - - bytes memory data = - abi.encodeCall(IBank.transfer_within, ("api.x.com", "bob", _idFor("bob"), address(0), 1 ether)); - // Build the execute call manually to use vm.expectRevert - uint256 currentNonce = alice.nonce(); - bytes32 digest = keccak256( - abi.encode(address(bank), uint256(0), keccak256(data), currentNonce, block.chainid, address(alice)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(ALICE_PK, digest); - vm.expectRevert(); - alice.execute(address(bank), 0, data, sigs); - } - - function test_webTransferNative() public { - _walletFor("github", "alice", ALICE_PK); - _walletFor("github", "bob", BOB_PK); - - // Deposit native for alice - bank.deposit{value: 10 ether}("api.github.com", "alice", _idFor("alice"), address(0), 10 ether); - - // Build webTransfer proof - _transferCounter++; - string memory resourceId = string(abi.encodePacked("repo/", vm.toString(_transferCounter), "/comment/1")); - string memory requestPath = - string(abi.encodePacked("/repos/owner/repo/issues/comments/", vm.toString(_transferCounter))); - bytes32 uid = keccak256(abi.encodePacked("api.github.com", ":", "issue_comment", ":", resourceId)); - bytes memory body = abi.encodePacked("@dyaka-agent honor @bob with 3 ETH"); - bytes memory authorBytes = bytes("alice"); - bytes memory authorId = abi.encodePacked('"author_id":"', _idFor("alice"), '"'); - - NotaryTlsProof memory notaryProof = - _buildBankProofWithId("api.github.com", requestPath, body, authorBytes, authorId, block.timestamp); - BackendSig memory backendSig = - _buildBackendSigWithId(uid, body, authorBytes, authorId, _idFor("bob"), block.timestamp); - - ResourceInfo memory resource = ResourceInfo({ - platform: "api.github.com", resourceType: "issue_comment", resourceId: resourceId, requestPath: requestPath - }); - - bank.webTransferV2( - resource, - SenderInfo({author: "alice", revealedAuthor: authorBytes, revealedAuthorId: authorId}), - body, - notaryProof, - backendSig, - "bob", - _idFor("bob"), - "ETH", - "3", - 3 ether, - "" - ); - - address aliceWallet = registry.resolve("api.github.com", "alice"); - address bobWallet = registry.resolve("api.github.com", "bob"); - assertEq(bank.balanceOf(aliceWallet, address(0)), 7 ether); - assertEq(bank.balanceOf(bobWallet, address(0)), 3 ether); - } - - function test_depositNative_wrongAmount_reverts() public { - _alice(); - vm.expectRevert(NativeAmountMismatch.selector); - bank.deposit{value: 1 ether}("api.x.com", "alice", _idFor("alice"), address(0), 2 ether); - } - - function test_balanceOfNative() public { - WebWallet alice = _alice(); - bank.deposit{value: 7 ether}("api.x.com", "alice", _idFor("alice"), address(0), 7 ether); - assertEq(bank.balanceOf("api.x.com", "alice", _idFor("alice"), address(0)), 7 ether); - assertEq(bank.balanceOf(address(alice), address(0)), 7 ether); - } - - function test_balanceOfTotalNative() public { - WebWallet alice = _alice(); - // Give wallet some native balance directly - vm.deal(address(alice), 3 ether); - bank.deposit{value: 5 ether}("api.x.com", "alice", _idFor("alice"), address(0), 5 ether); - - // balanceOfTotal should include wallet's native balance + bank balance - assertEq(bank.balanceOfTotal(address(alice), address(0)), 5 ether + 3 ether); - assertEq(bank.balanceOfTotal("api.x.com", "alice", _idFor("alice"), address(0)), 5 ether + 3 ether); - } - - function test_walletSendNativeToEOA() public { - WebWallet alice = _alice(); - vm.deal(address(alice), 10 ether); - address recipient = address(0xBEEF); - - // Alice sends ETH to recipient via execute() with value - _execute(alice, recipient, 2 ether, "", ALICE_PK); - - assertEq(recipient.balance, 2 ether); - assertEq(address(alice).balance, 8 ether); - } - - // ═══════════════════════════════════════════════════════════════ - // MUTATION-GUARD NEGATIVE TESTS - // ═══════════════════════════════════════════════════════════════ - - // 16. Withdraw native to a contract whose receive() reverts → the low-level - // call fails → NativeTransferFailed. Removing the guard would silently - // "succeed" (funds debited, transfer failed), killing the mutant. - function test_NativeTransferFailed_reverts() public { - WebWallet alice = _alice(); - bank.deposit{value: 5 ether}("api.x.com", "alice", _idFor("alice"), address(0), 5 ether); - - RejectEther sink = new RejectEther(); - vm.prank(address(alice)); - vm.expectRevert(NativeTransferFailed.selector); - bank.withdraw(address(sink), address(0), 3 ether); - } - - // 17. Re-registering the native asset (address(0), already registered in setUp) - // hits the native branch of AdminFacet.registerToken → AlreadyRegistered. - function test_AlreadyRegistered_native_reverts() public { - // "ETH" -> address(0) was registered in setUp. - vm.expectRevert(AlreadyRegistered.selector); - bank.registerToken("ETH", address(0)); - } - - receive() external payable {} -} diff --git a/solidity/archived/transfer/test/PausableFlow.t.sol b/solidity/archived/transfer/test/PausableFlow.t.sol deleted file mode 100644 index d464d81a..00000000 --- a/solidity/archived/transfer/test/PausableFlow.t.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import {Registry} from "../../login/Registry.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {ResourceInfo, SenderInfo, BackendSig, NotaryTlsProof} from "../bank/BankTypes.sol"; -import {EnforcedPause, InsufficientBalance} from "../bank/BankErrors.sol"; - -/// @notice Pausable emergency-stop / migration-cutover gate on Registry + Bank. -/// Proves: pause/unpause are owner-only; a gated entrypoint reverts -/// `EnforcedPause` while paused and resumes its normal behaviour after -/// unpause; admin/owner functions stay callable while paused. -contract PausableFlowTest is Test, BankDiamondDeployer { - Registry registry; - IBank bank; - address owner = address(this); - address notOwner = address(0xBEEF); - - function setUp() public { - Registry rImpl = new Registry(); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - // notary, backend, walletFactory, owner — all must be non-zero. - abi.encodeCall(Registry.initialize, (address(0x11), address(0x12), address(0x13), owner)) - ) - ) - ); - // real registry so withdraw's lazy _migrate (getUserIds) is a no-op, not a call to a codeless addr. - bank = IBank(deployBankDiamond(owner, address(0x21), address(0x22), address(registry))); - } - - // ── access control ───────────────────────────────────────────── - - function test_pause_onlyOwner() public { - vm.prank(notOwner); - vm.expectRevert(); - registry.pause(); - - vm.prank(notOwner); - vm.expectRevert(); - bank.pause(); - } - - function test_unpause_onlyOwner() public { - registry.pause(); - bank.pause(); - vm.prank(notOwner); - vm.expectRevert(); - registry.unpause(); - vm.prank(notOwner); - vm.expectRevert(); - bank.unpause(); - } - - // ── Registry: gated when paused, normal after unpause ────────── - - function test_registry_pausedBlocksRegistration() public { - registry.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - registry.register_session_zk("api.x.com", ""); - - // Gate lifted → reaches the real path (no verifier configured). - registry.unpause(); - vm.expectRevert(Registry.ZkVerifierNotSet.selector); - registry.register_session_zk("api.x.com", ""); - } - - // ── Bank: gated when paused, normal after unpause ────────────── - - function test_bank_pausedBlocksWithdraw() public { - bank.pause(); - vm.expectRevert(EnforcedPause.selector); - bank.withdraw(owner, address(0), 1); - - // Gate lifted → reaches the real path (no balance to withdraw). - bank.unpause(); - vm.expectRevert(InsufficientBalance.selector); - bank.withdraw(owner, address(0), 1); - } - - /// Both webTransferV2 overloads move user funds, so a pause must block them - /// before any proof work. whenNotPaused runs ahead of proof validation, so a - /// paused call reverts EnforcedPause even with empty proof structs. - function test_bank_pausedBlocksWebTransfer() public { - ResourceInfo memory resource = ResourceInfo("api.x.com", "", "", ""); - NotaryTlsProof memory tls = NotaryTlsProof( - "", - bytes32(0), - bytes32(0), - bytes32(0), - "", - bytes32(0), - 0, - new bytes32[](0), - new bytes32[](0), - new bytes32[](0), - new bytes32[](0), - new bytes32[](0), - new bytes32[](0), - "", - new bytes32[](0), - "", - new bytes32[](0), - "" - ); - BackendSig memory sig = BackendSig("", 0); - - bank.pause(); - - // token-by-name overload - vm.expectRevert(EnforcedPause.selector); - bank.webTransferV2( - resource, SenderInfo("author", "", ""), "", tls, sig, "bob", "id_bob", "$X", "1", 1, "https://x.com/p/1" - ); - - // token-by-address overload - vm.expectRevert(EnforcedPause.selector); - bank.webTransferV2( - resource, - SenderInfo("author", "", ""), - "", - tls, - sig, - "bob", - "id_bob", - address(0xCAFE), - "1", - 1, - "https://x.com/p/1" - ); - } - - // ── admin/owner fns stay callable while paused ──────────────── - - function test_adminWorksWhilePaused() public { - registry.pause(); - bank.pause(); - - // Neither carries whenNotPaused — must still work (so migrate/rotate - // can run during a cutover pause). - registry.setBackend(address(0x99)); - bank.registerToken("$X", address(0xCAFE)); - - assertEq(registry.backend(), address(0x99), "admin setBackend blocked by pause"); - assertTrue(registry.paused(), "registry should be paused"); - assertTrue(bank.paused(), "bank should still be paused"); - // A whenNotPaused entrypoint (withdraw) still reverts EnforcedPause. - vm.expectRevert(EnforcedPause.selector); - bank.withdraw(owner, address(0), 1); - } -} diff --git a/solidity/archived/transfer/test/RegistryBankRecycle.t.sol b/solidity/archived/transfer/test/RegistryBankRecycle.t.sol deleted file mode 100644 index 236d8a55..00000000 --- a/solidity/archived/transfer/test/RegistryBankRecycle.t.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {RegistryTest} from "../../login/test/Registry.t.sol"; - -/// Registry × Bank integration. Lives here, not in Registry.t.sol, so the -/// login-side Registry tests carry no Bank dependency. Inherits RegistryTest -/// for its fixtures and `_registerXAliceId` helper. -contract RegistryBankRecycleTest is RegistryTest, BankDiamondDeployer { - // T12: Bank balance under A's id survives a handle recycle to B (I2/I3). - function test_T12_bankBalanceSurvivesRecycle() public { - // A Bank diamond wired to this registry. - IBank bank = IBank(deployBankDiamond(owner, notaryAddr, backendAddr, address(registry))); - bank.registerToken("ETH", address(0)); - - // A registers x.com/alice under id 100 and is funded. - address walletA = _registerXAliceId("100", userSessionAddr, block.timestamp); - bank.deposit{value: 1 ether}("x.com", "alice", "100", address(0), 1 ether); - assertEq(bank.balanceOf(walletA, address(0)), 1 ether); - - // B recycles the handle "alice" under a fresh id 200. - uint256 bPk = 0xB003; - address bAddr = vm.addr(bPk); - address walletB = _registerXAliceId("200", bAddr, block.timestamp); - - // Resolve now points to B, but A's funds are intact and reachable by id. - assertEq(registry.resolve("x.com", "alice"), walletB); - assertEq(registry.resolveById("x.com", "100"), walletA); - assertEq(bank.balanceOf(walletA, address(0)), 1 ether); - assertEq(bank.balanceOf(walletB, address(0)), 0); - } -} diff --git a/solidity/archived/transfer/test/TemplateMatching.t.sol b/solidity/archived/transfer/test/TemplateMatching.t.sol deleted file mode 100644 index 74ecfc7d..00000000 --- a/solidity/archived/transfer/test/TemplateMatching.t.sol +++ /dev/null @@ -1,372 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {TemplateMissingToken, TemplateMissingAmount, TemplateMissingTokenAfterRecipient} from "../bank/BankErrors.sol"; -import {LibTemplate} from "../bank/libraries/LibTemplate.sol"; -import {LibString} from "../bank/libraries/LibString.sol"; -import {LibTemplateStorage} from "../bank/storage/LibTemplateStorage.sol"; - -/// @dev Unit harness for the template/id libraries the diamond actually runs. It -/// calls `LibTemplate`/`LibString` directly (its own diamond-storage slot), -/// so these tests exercise the SAME code the diamond does — no legacy Bank. -contract LibTemplateHarness { - function setPlatformTemplate(string calldata platform, string calldata tmpl) external { - LibTemplate.addTemplate(platform, tmpl); - } - - function platformTemplates(bytes32 key, uint256 i) external view returns (string memory) { - return LibTemplateStorage.store().platformTemplates[key][i]; - } - - function matchTemplate(bytes calldata data, bytes32 key, uint256 i) - external - view - returns (bool, string memory, string memory, string memory) - { - return LibTemplate.matchTemplate(data, key, i); - } - - function matchAnyTemplate(bytes calldata data, bytes32 key) - external - view - returns (bool, string memory, string memory) - { - (bool matched, string memory amt, string memory tok,) = LibTemplate.matchAnyTemplate(data, key); - return (matched, amt, tok); - } - - function extractId(bytes calldata s) external pure returns (string memory) { - return LibString.extractId(s); - } -} - -contract TemplateMatchingTest is Test, BankDiamondDeployer { - // `bank` is the diamond. `harness` calls the same libraries directly for the - // internal matcher/extractId paths the diamond has no external surface for. - IBank bank; - LibTemplateHarness harness; - - function setUp() public { - bank = IBank(deployBankDiamond(address(this), address(1), address(2), address(3))); - harness = new LibTemplateHarness(); - // Mirror the diamond's seeded templates into the harness so the library - // matching tests run against the exact same defaults (no hardcoding). - _mirror("api.x.com"); - _mirror("api.github.com"); - } - - function _mirror(string memory platform) internal { - uint256 n = bank.platformTemplateCount(platform); - for (uint256 i = 0; i < n; i++) { - harness.setPlatformTemplate(platform, bank.getPlatformTemplate(platform, i)); - } - } - - // ─── Template Parsing ───────────────────────────────────────── - - function test_defaultGithubTemplateParts_withOf() public view { - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("api.github.com", 0); - assertEq(prefix, "@dyaka-agent honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " of "); - assertEq(suffix, ""); - } - - function test_defaultGithubTemplateParts_withoutOf() public view { - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("api.github.com", 1); - assertEq(prefix, "@dyaka-agent honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " "); - assertEq(suffix, ""); - } - - function test_defaultXTemplateParts_withOf() public view { - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("api.x.com", 0); - assertEq(prefix, "@dyaka_agent honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " of "); - assertEq(suffix, ""); - } - - function test_defaultXTemplateParts_withoutOf() public view { - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("api.x.com", 1); - assertEq(prefix, "@dyaka_agent honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " "); - assertEq(suffix, ""); - } - - /// The diamond stores the raw template string verbatim, read via getPlatformTemplate. - function test_fullTemplateStringStored() public view { - assertEq( - bank.getPlatformTemplate("api.github.com", 0), "@dyaka-agent honor @{recipient} with {amount} of {token}" - ); - assertEq(bank.getPlatformTemplate("api.github.com", 1), "@dyaka-agent honor @{recipient} with {amount} {token}"); - } - - function test_setPlatformTemplateViaAdmin() public { - bank.setPlatformTemplate("newplatform", "@bot honor @{recipient} with {amount} {token}"); - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("newplatform", 0); - assertEq(prefix, "@bot honor @"); - assertEq(afterRecipient, " with "); - assertEq(afterAmount, " "); - assertEq(suffix, ""); - assertEq(bank.getPlatformTemplate("newplatform", 0), "@bot honor @{recipient} with {amount} {token}"); - } - - function test_templateWithMiddleWord() public { - bank.setPlatformTemplate("test", "@libid-bot send @{recipient} then {amount} of {token}"); - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("test", 0); - assertEq(prefix, "@libid-bot send @"); - assertEq(afterRecipient, " then "); - assertEq(afterAmount, " of "); - assertEq(suffix, ""); - } - - function test_templateWithSuffix() public { - bank.setPlatformTemplate("test", "pay @{recipient} now {amount} in {token} done"); - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("test", 0); - assertEq(prefix, "pay @"); - assertEq(afterRecipient, " now "); - assertEq(afterAmount, " in "); - assertEq(suffix, " done"); - } - - /// A template with {amount}/{token} but NO {recipient} is a recipient-less - /// placeholder template (recipient inferred off-chain): prefix runs up to - /// {amount} and afterRecipient is empty. - function test_setTemplateWithoutRecipient_isRecipientLess() public { - bank.setPlatformTemplate("implicit", "@dyaka-agent honor with {amount} {token}"); - (string memory prefix, string memory afterRecipient, string memory afterAmount, string memory suffix) = - bank.getTemplateParts("implicit", 0); - assertEq(prefix, "@dyaka-agent honor with "); - assertEq(afterRecipient, ""); - assertEq(afterAmount, " "); - assertEq(suffix, ""); - } - - /// A recipient-less template still requires {token} after {amount}. - function test_setTemplateRecipientLessRequiresToken() public { - vm.expectRevert(TemplateMissingToken.selector); - bank.setPlatformTemplate("bad", "@dyaka-agent honor with {amount}"); - } - - /// A template with no placeholders at all stays a simple literal. - function test_setTemplateNoPlaceholders_isLiteral() public { - bank.setPlatformTemplate("simple", "@dyaka-agent gm fam"); - (string memory prefix,,,) = bank.getTemplateParts("simple", 0); - assertEq(prefix, "@dyaka-agent gm fam"); - } - - function test_setTemplateRequiresAmountPlaceholder() public { - vm.expectRevert(TemplateMissingAmount.selector); - bank.setPlatformTemplate("bad", "@dyaka-agent honor @{recipient} with {token}"); - } - - function test_setTemplateRequiresTokenPlaceholder() public { - vm.expectRevert(TemplateMissingTokenAfterRecipient.selector); - bank.setPlatformTemplate("bad", "@dyaka-agent honor @{recipient} with {amount}"); - } - - function test_setTemplateRecipientMustBeBeforeAmount() public { - vm.expectRevert(TemplateMissingAmount.selector); - bank.setPlatformTemplate("bad", "@dyaka-agent {amount} @{recipient} {token}"); - } - - // ─── Multiple Templates ────────────────────────────────────── - - function test_multipleTemplatesPerPlatform() public { - bank.setPlatformTemplate("multi", "simple template A"); - bank.setPlatformTemplate("multi", "simple template B"); - assertEq(bank.platformTemplateCount("multi"), 2); - assertEq(bank.getPlatformTemplate("multi", 0), "simple template A"); - assertEq(bank.getPlatformTemplate("multi", 1), "simple template B"); - } - - function test_clearPlatformTemplates() public { - bank.setPlatformTemplate("clearme", "template A"); - bank.setPlatformTemplate("clearme", "template B"); - assertEq(bank.platformTemplateCount("clearme"), 2); - bank.clearPlatformTemplates("clearme"); - assertEq(bank.platformTemplateCount("clearme"), 0); - } - - function test_matchAnyTemplate_matchesSecondTemplate() public { - harness.setPlatformTemplate("multi", "first pattern"); - harness.setPlatformTemplate("multi", "second pattern"); - bytes memory data = bytes("this contains second pattern here"); - (bool ok,,) = harness.matchAnyTemplate(data, keccak256("multi")); - assertTrue(ok); - } - - function test_matchAnyTemplate_noMatch() public { - harness.setPlatformTemplate("multi", "first pattern"); - harness.setPlatformTemplate("multi", "second pattern"); - bytes memory data = bytes("this matches nothing"); - (bool ok,,) = harness.matchAnyTemplate(data, keccak256("multi")); - assertFalse(ok); - } - - // ─── Template Matching (JSON delimiters) ────────────────────── - - function test_matchTemplate_plainText_withoutOf() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.0001 $TIA"); - // Template index 1 = without "of" - (bool ok, string memory amount, string memory token,) = harness.matchTemplate(data, keccak256("api.x.com"), 1); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_plainText_withOf() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.0001 of $TIA"); - // Template index 0 = with "of" - (bool ok, string memory amount, string memory token,) = harness.matchTemplate(data, keccak256("api.x.com"), 0); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchAnyTemplate_withOf() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.0001 of $TIA"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchAnyTemplate_withoutOf() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.0001 $TIA"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_textBeforeCommand() public view { - bytes memory data = bytes( - "This is a comment with some text before the template.\n\n@dyaka_agent honor @alice with 0.0001 of $TIA, because we are generous." - ); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_textBeforeCommand_github() public view { - bytes memory data = bytes( - "This is the comment with some works before the main template.\n\n@dyaka-agent honor @alice-test-account with 0.0001 of $TIA, because we are generous." - ); - (bool ok, string memory amount, string memory token) = - harness.matchAnyTemplate(data, keccak256("api.github.com")); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_jsonEncoded() public view { - bytes memory data = bytes('@dyaka_agent honor @alice with 0.0001 $TIA"'); - (bool ok, string memory amount, string memory token,) = harness.matchTemplate(data, keccak256("api.x.com"), 1); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_jsonWithCommaAfterToken() public view { - bytes memory data = bytes('@dyaka_agent honor @alice with 0.0001 $TIA","next_field'); - (bool ok, string memory amount, string memory token,) = harness.matchTemplate(data, keccak256("api.x.com"), 1); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_jsonWithBraceAfterToken() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.0001 $TIA}"); - (bool ok, string memory amount, string memory token,) = harness.matchTemplate(data, keccak256("api.x.com"), 1); - assertTrue(ok); - assertEq(amount, "0.0001"); - assertEq(token, "$TIA"); - } - - // ─── Token character whitelist tests ───────────────────────── - - function test_matchTemplate_trailingExclamation() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.1 $TIA! great work"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "0.1"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_trailingQuestion() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.1 $TIA?"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "0.1"); - assertEq(token, "$TIA"); - } - - function test_matchTemplate_trailingPeriodNewline() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 0.1 $TIA.\nMore text"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - // Dot is a valid token char, so it gets included - assertEq(amount, "0.1"); - assertEq(token, "$TIA."); - } - - function test_matchTemplate_dottedToken() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 100 USDC.e"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "100"); - assertEq(token, "USDC.e"); - } - - function test_matchTemplate_dottedTokenInSentence() public view { - bytes memory data = bytes("@dyaka_agent honor @alice with 100 USDC.e for her work"); - (bool ok, string memory amount, string memory token) = harness.matchAnyTemplate(data, keccak256("api.x.com")); - assertTrue(ok); - assertEq(amount, "100"); - assertEq(token, "USDC.e"); - } - - // ── extractId (sender id parsing) ───────────────────────────── - - function test_extractId_quoted() public view { - assertEq(harness.extractId(bytes('"author_id":"1234567890"')), "1234567890"); - } - - function test_extractId_bareComma() public view { - assertEq(harness.extractId(bytes('"id":123,')), "123"); - } - - function test_extractId_bareBrace() public view { - assertEq(harness.extractId(bytes('"id":456}')), "456"); - } - - function test_extractId_quotedNonNumeric() public view { - // Quoted ids are taken verbatim (X snowflakes, or test ids like id_alice). - assertEq(harness.extractId(bytes('"author_id":"id_alice"')), "id_alice"); - } - - function test_extractId_malformed_reverts() public { - vm.expectRevert(LibString.MalformedAuthorId.selector); - harness.extractId(bytes("noColonHere")); - } - - function test_extractId_emptyBare_reverts() public { - vm.expectRevert(LibString.EmptyAuthorId.selector); - harness.extractId(bytes('"id":,')); - } -} diff --git a/solidity/archived/transfer/test/TransferWithin.t.sol b/solidity/archived/transfer/test/TransferWithin.t.sol deleted file mode 100644 index a0a3b7d7..00000000 --- a/solidity/archived/transfer/test/TransferWithin.t.sol +++ /dev/null @@ -1,364 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import {Registry} from "../../login/Registry.sol"; -import {IBank} from "../bank/IBank.sol"; -import {BankDiamondDeployer} from "../script/BankDiamondDeployer.sol"; -import {SenderNotRegistered, ZeroAmount, InsufficientBalance, InsufficientAllowance} from "../bank/BankErrors.sol"; -import {Notary} from "../../notary/Notary.sol"; -import {deployNotary} from "../../notary/test/DeployNotary.sol"; -import {WalletFactory} from "../../login/WalletFactory.sol"; -import {WebWallet} from "../../login/WebWallet.sol"; -import {MockERC20} from "../MockERC20.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -contract TransferWithinTest is Test, BankDiamondDeployer { - uint256 constant NOTARY_KEY = uint256(keccak256("notary")); - uint256 constant BACKEND_KEY = uint256(keccak256("backend")); - - address NOTARY; - address BACKEND; - - Registry registry; - IBank bank; - WalletFactory factory; - MockERC20 token; - - uint256 constant ALICE_PK = 0xA001; - uint256 constant BOB_PK = 0xB001; - uint256 constant CHARLIE_PK = 0xC001; - - function setUp() public { - NOTARY = vm.addr(NOTARY_KEY); - BACKEND = vm.addr(BACKEND_KEY); - - WebWallet walletImpl = new WebWallet(); - Registry rImpl = new Registry(); - WalletFactory fImpl = new WalletFactory(); - - factory = WalletFactory( - address( - new ERC1967Proxy( - address(fImpl), - abi.encodeCall(WalletFactory.initialize, (address(this), address(walletImpl), address(1))) - ) - ) - ); - - Notary notaryReg = deployNotary(address(this), NOTARY); - registry = Registry( - address( - new ERC1967Proxy( - address(rImpl), - abi.encodeCall(Registry.initialize, (address(notaryReg), BACKEND, address(factory), address(this))) - ) - ) - ); - - factory.setRegistry(address(registry)); - bank = IBank(deployBankDiamond(address(this), address(notaryReg), BACKEND, address(registry))); - token = new MockERC20("Test", "TST"); - bank.registerToken("TST", address(token)); - - // Register platforms used in tests (X-style quoted id binding). - registry.setPlatform("api.x.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - registry.setPlatform("api.github.com", "api.test.com/endpoint", '"username":"', '"id":"', '"'); - - vm.warp(1_000_000); - } - - /// Deterministic immutable id for a handle (escrow keyed by id). - function _idFor(string memory handle) internal pure returns (string memory) { - return string(abi.encodePacked("id_", handle)); - } - - // ─── Helpers ──────────────────────────────────────────────────── - - /// Map platform shortname to API domain for Registry registration. - function _domain(string memory platform) internal pure returns (string memory) { - if (keccak256(bytes(platform)) == keccak256("x")) return "api.x.com"; - if (keccak256(bytes(platform)) == keccak256("github")) return "api.github.com"; - return platform; - } - - function _walletFor(string memory platform, string memory handle, uint256 pk) internal returns (WebWallet) { - string memory domain = _domain(platform); - string memory userId = _idFor(handle); - address sessionAddr = vm.addr(pk); - Registry.FullTlsProof memory proof = _buildRegistryProof( - domain, handle, userId, "api.test.com/endpoint", '"username":"', sessionAddr, block.timestamp - ); - registry.register_session(proof, domain, handle, userId, "api.test.com/endpoint"); - return WebWallet(payable(registry.resolve(domain, handle))); - } - - function _fund(string memory platform, string memory handle, uint256 amount) internal { - token.mint(address(this), amount); - token.approve(address(bank), amount); - bank.deposit(_domain(platform), handle, _idFor(handle), address(token), amount); - } - - function _fundWallet(WebWallet wallet, uint256 amount) internal { - token.mint(address(wallet), amount); - } - - function _execute(WebWallet wallet, address target, bytes memory data, uint256 pk) internal { - uint256 currentNonce = wallet.nonce(); - bytes32 digest = - keccak256(abi.encode(target, uint256(0), keccak256(data), currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(pk, digest); - wallet.execute(target, 0, data, sigs); - } - - function _executeBatch( - WebWallet wallet, - address[] memory targets, - uint256[] memory values, - bytes[] memory datas, - uint256 pk - ) internal { - uint256 currentNonce = wallet.nonce(); - bytes32 dataHashes = keccak256(abi.encode(datas)); - bytes32 digest = - keccak256(abi.encode(targets, values, dataHashes, currentNonce, block.chainid, address(wallet))); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(pk, digest); - wallet.executeBatch(targets, values, datas, sigs); - } - - function _sign(uint256 pk, bytes32 digest) internal pure returns (bytes memory) { - bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethHash); - return abi.encodePacked(r, s, v); - } - - function _leaf(string memory prefix, bytes memory value) internal pure returns (bytes32) { - return keccak256(bytes.concat(keccak256(abi.encodePacked(prefix, value)))); - } - - function _h(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); - } - - // 4-leaf tree (D, U, E, Id) — id bound via the quoted "id":"" recv leaf. - function _buildRegistryProof( - string memory domain, - string memory username, - string memory userId, - string memory endpoint, - string memory handlePrefix, - address userAddress, - uint256 ts - ) internal view returns (Registry.FullTlsProof memory proof) { - bytes32 clientRandom = bytes32(uint256(111)); - bytes32 serverRandom = bytes32(uint256(222)); - bytes memory serverEphKey = hex"deadbeef"; - bytes32 domainHash = keccak256(bytes(domain)); - - bytes32 leafD = _leaf("domain:", bytes(domain)); - bytes32 leafU = _leaf("recv:", abi.encodePacked(handlePrefix, username, '"')); - bytes32 leafE = _leaf("endpoint:", bytes(endpoint)); - bytes32 leafId = _leaf("recv:", abi.encodePacked('"id":"', userId, '"')); - - bytes32 hDU = _h(leafD, leafU); - bytes32 hEId = _h(leafE, leafId); - bytes32 transcriptRoot = _h(hDU, hEId); - - bytes32[] memory dp = new bytes32[](2); - dp[0] = leafU; - dp[1] = hEId; - bytes32[] memory up = new bytes32[](2); - up[0] = leafD; - up[1] = hEId; - bytes32[] memory ep = new bytes32[](2); - ep[0] = leafId; - ep[1] = hDU; - bytes32[] memory idp = new bytes32[](2); - idp[0] = leafE; - idp[1] = hDU; - - bytes32 notaryDigest = keccak256( - abi.encode( - block.chainid, - address(registry), - domainHash, - clientRandom, - serverRandom, - keccak256(serverEphKey), - transcriptRoot, - ts - ) - ); - bytes32 backendDigest = keccak256(abi.encode(userAddress, address(0), transcriptRoot, ts)); - - proof = Registry.FullTlsProof({ - notarySignature: _sign(NOTARY_KEY, notaryDigest), - backendSignature: _sign(BACKEND_KEY, backendDigest), - userAddress: userAddress, - walletAddress: address(0), - domainHash: domainHash, - clientRandom: clientRandom, - serverRandom: serverRandom, - serverEphemeralKey: serverEphKey, - transcriptRoot: transcriptRoot, - timestamp: ts, - domainPath: dp, - usernamePath: up, - endpointPath: ep, - idPath: idp - }); - } - - // ─── transfer_within via execute helper ───────────────────────── - - function _transferWithin( - WebWallet sender, - uint256 senderPk, - string memory receiverPlatform, - string memory receiverHandle, - uint256 amount - ) internal { - bytes memory data = abi.encodeCall( - IBank.transfer_within, - (_domain(receiverPlatform), receiverHandle, _idFor(receiverHandle), address(token), amount) - ); - _execute(sender, address(bank), data, senderPk); - } - - // ─── Tests ────────────────────────────────────────────────────── - - /// @notice Happy path: registered sender → registered receiver - function test_transferWithin_registeredToRegistered() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - WebWallet bob = _walletFor("x", "bob", BOB_PK); - - _fund("x", "alice", 1000e18); - - _transferWithin(alice, ALICE_PK, "x", "bob", 400e18); - - assertEq(bank.registeredBalances(address(alice), address(token)), 600e18); - assertEq(bank.registeredBalances(address(bob), address(token)), 400e18); - } - - /// @notice Transfer to unregistered receiver goes to unregisteredBalances - function test_transferWithin_toUnregistered() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - - _fund("x", "alice", 500e18); - - _transferWithin(alice, ALICE_PK, "x", "charlie", 200e18); - - assertEq(bank.registeredBalances(address(alice), address(token)), 300e18); - bytes32 key = keccak256(abi.encode("id:v1", "api.x.com", _idFor("charlie"))); - assertEq(bank.unregisteredBalances(key, address(token)), 200e18); - } - - /// @notice Auto-top-up: Bank balance insufficient, pulls from WebWallet ERC20 - function test_transferWithin_autoTopUp() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - WebWallet bob = _walletFor("x", "bob", BOB_PK); - - // Give alice 200 in bank, 300 as ERC20 on wallet - _fund("x", "alice", 200e18); - _fundWallet(alice, 300e18); - - // Approve bank to pull from wallet - bytes memory approveData = abi.encodeCall(IERC20.approve, (address(bank), type(uint256).max)); - _execute(alice, address(token), approveData, ALICE_PK); - - // Transfer 400 — needs 200 shortfall from ERC20 - _transferWithin(alice, ALICE_PK, "x", "bob", 400e18); - - assertEq(bank.registeredBalances(address(alice), address(token)), 0); - assertEq(bank.registeredBalances(address(bob), address(token)), 400e18); - assertEq(token.balanceOf(address(alice)), 100e18); // 300 - 200 shortfall - } - - /// @notice Fails if sender is not registered - function test_transferWithin_senderNotRegistered_reverts() public { - _walletFor("x", "bob", BOB_PK); - - // Call from an unregistered address - vm.prank(address(0xdead)); - vm.expectRevert(SenderNotRegistered.selector); - bank.transfer_within("x", "bob", "", address(token), 100e18); - } - - /// @notice Fails if insufficient balance and no ERC20 approved - function test_transferWithin_insufficientAndNoApproval_reverts() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - - _fund("x", "alice", 100e18); - - // Try to transfer 200 — shortfall 100, but no approval → revert - bytes memory data = abi.encodeCall(IBank.transfer_within, ("x", "bob", "", address(token), 200e18)); - uint256 currentNonce = alice.nonce(); - bytes32 digest = keccak256( - abi.encode(address(bank), uint256(0), keccak256(data), currentNonce, block.chainid, address(alice)) - ); - bytes[] memory sigs = new bytes[](1); - sigs[0] = _sign(ALICE_PK, digest); - - vm.expectRevert(); // safeTransferFrom will revert (no approval/balance) - alice.execute(address(bank), 0, data, sigs); - } - - /// @notice Zero amount reverts - function test_transferWithin_zeroAmount_reverts() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - - vm.prank(address(alice)); - vm.expectRevert(ZeroAmount.selector); - bank.transfer_within("x", "bob", "", address(token), 0); - } - - // ═══════════════════════════════════════════════════════════════ - // MUTATION-GUARD NEGATIVE TESTS - // ═══════════════════════════════════════════════════════════════ - - // 10. transfer_within from a caller that is NOT a registered wallet (empty - // getHandles) → SenderNotRegistered. The caller holds tokens + approval so - // removing the guard would SUCCEED (auto-top-up pulls), killing the mutant. - function test_SenderNotRegistered_transferWithin_reverts() public { - _walletFor("x", "bob", BOB_PK); // registered receiver - - address stranger = address(0xBEEF); - token.mint(stranger, 100e18); - vm.prank(stranger); - token.approve(address(bank), 100e18); - - vm.prank(stranger); - vm.expectRevert(SenderNotRegistered.selector); - bank.transfer_within("api.x.com", "bob", _idFor("bob"), address(token), 100e18); - } - - // 18. transfer_within NATIVE (address(0)) with bank balance < amount: native - // can't be pulled → LibEscrow.autoTopUp reverts InsufficientBalance. - function test_InsufficientBalance_reverts() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - - vm.prank(address(alice)); - vm.expectRevert(InsufficientBalance.selector); - bank.transfer_within("api.x.com", "bob", _idFor("bob"), address(0), 1 ether); - } - - // 19. transfer_within ERC20 with bank balance < amount AND no allowance → the - // shortfall pull can't be authorized → LibEscrow.autoTopUp reverts the - // custom InsufficientAllowance (removing the guard yields the OZ ERC20 error). - function test_InsufficientAllowance_reverts() public { - WebWallet alice = _walletFor("x", "alice", ALICE_PK); - _walletFor("x", "bob", BOB_PK); - _fund("x", "alice", 100e18); // bank balance 100; transfer 200 → shortfall 100 - - // Parametrized error → match on the selector prefix (removing the guard - // yields the OZ ERC20 allowance error, a different selector). - vm.prank(address(alice)); - vm.expectPartialRevert(InsufficientAllowance.selector); - bank.transfer_within("api.x.com", "bob", _idFor("bob"), address(token), 200e18); - } -} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 1743d5b6..90f77ae6 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -4,8 +4,7 @@ out = "out" libs = ["lib"] # Tests live beside the code they cover — contracts/ceremony/test, contracts/ # identity/test, contracts/factory/test — so discovery goes through src -# rather than one test directory. archived/ is outside every root on -# purpose: what it holds is reference, not source (see archived/README.md). +# rather than one test directory. test = "contracts" solc = "0.8.33" evm_version = "cancun" From 4d97ac3ea565badc875461f82ce247ec45dd7750 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 11:34:52 +0100 Subject: [PATCH 06/15] refactor(ceremony)!: IdentityJwksRoots is GoogleJwtRoots, beside the verifier it serves The list of Google signing keys served exactly one reader, the `google/v1` Platform Verifier, yet lived under contracts/identity/ with "Identity" in its name -- a placement that said nothing about what it was for and put it one directory away from the contract that reads it. It is now `GoogleJwtRoots` in contracts/ceremony/, with the test and the real-body fixture moved alongside, and the ERC-7201 namespace follows the name: `libid.storage.GoogleJwtRoots`, slot recomputed and pinned in UpgradeSafety. A fresh testnet deployment follows, so no slot compatibility with the old namespace is owed. The verifier's side of the surface is renamed to match (IGoogleJwtRoots, jwtRoots / setJwtRoots / JwtRootsChanged), as are the deploy script's GOOGLE_JWT_ROOTS_ADDRESS line, the Rust binding (now in bindings::ceremony, so identity.rs is IdentityNames alone), the vendored artifact list, the TS ABI export (googleJwtRootsAbi, calls.googleJwtRoots) and every README and crate description that named the old contract. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- README.md | 11 +- rust/contracts/Cargo.toml | 2 +- rust/contracts/README.md | 18 +- rust/contracts/src/artifacts.rs | 2 +- rust/contracts/src/bindings/ceremony.rs | 71 +++++++- rust/contracts/src/bindings/identity.rs | 66 +------ rust/contracts/src/lib.rs | 8 +- rust/contracts/tests/anvil.rs | 16 +- scripts/vendor-artifacts.sh | 2 +- .../GoogleJwtRoots.sol} | 43 ++--- .../ceremony/GooglePlatformVerifier.sol | 29 +-- .../test/GoogleJwtRoots.t.sol} | 171 +++++++++--------- .../test/GooglePlatformVerifier.t.sol | 18 +- .../ceremony/test/UpgradeSafety.t.sol | 22 +-- .../test/fixtures/google-jwks.json | 0 solidity/foundry.toml | 4 +- solidity/script/Deploy.s.sol | 20 +- ts/packages/contracts/README.md | 8 +- ts/packages/contracts/scripts/codegen.mjs | 2 +- ts/packages/contracts/src/calls.test.ts | 6 +- 20 files changed, 258 insertions(+), 261 deletions(-) rename solidity/contracts/{identity/IdentityJwksRoots.sol => ceremony/GoogleJwtRoots.sol} (97%) rename solidity/contracts/{identity/test/IdentityJwksRoots.t.sol => ceremony/test/GoogleJwtRoots.t.sol} (88%) rename solidity/contracts/{identity => ceremony}/test/fixtures/google-jwks.json (100%) diff --git a/README.md b/README.md index 6eedab42..5bebc42c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Smart contracts for libID, laid out per chain. `solidity/` is a self-contained Foundry project holding the EVM contracts: the ceremony verification path -(Notary Service, Proof Verifier, Platform Verifiers), the identity naming -system with its Google JWKS root list, and the deterministic deployment -factory. `rust/` and `ts/` hold the ABI wrapper packages; room is reserved +(Notary Service, Proof Verifier, Platform Verifiers, and the Google JWT root +list the Google verifier reads), the identity naming system, and the +deterministic deployment factory. `rust/` and `ts/` hold the ABI wrapper packages; room is reserved for `solana/` and other networks. ## Layout @@ -12,8 +12,9 @@ for `solana/` and other networks. ``` solidity/ # Foundry project root contracts/ - ceremony/ # NotaryService, CeremonyProofVerifier, Platform Verifiers - identity/ # IdentityNames, IdentityJwksRoots, handle normalization + ceremony/ # NotaryService, CeremonyProofVerifier, Platform Verifiers, + # GoogleJwtRoots + identity/ # IdentityNames, handle normalization factory/ # LibidFactory: deterministic CREATE3 deployment WTIA9.sol # wrapped TIA script/Deploy.s.sol diff --git a/rust/contracts/Cargo.toml b/rust/contracts/Cargo.toml index 638542a2..84b1322d 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, identity names, Google JWKS roots, deterministic factory)." +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)." keywords = ["ethereum", "alloy", "contracts", "deployment"] categories = ["cryptography::cryptocurrencies"] # The vendored artifacts are part of the crate: consumers deploy from the diff --git a/rust/contracts/README.md b/rust/contracts/README.md index ea723941..895c1891 100644 --- a/rust/contracts/README.md +++ b/rust/contracts/README.md @@ -2,9 +2,9 @@ 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`), the -naming system (`IdentityNames`) with its Google JWKS trust list -(`IdentityJwksRoots`), and the deterministic deployment factory +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`). The compiled artifacts are vendored into the crate, so a consumer can deploy @@ -17,12 +17,12 @@ until it exists `cargo build` fails at macro expansion. Signing stays on the consumer's side: every helper is generic over an alloy `Provider` you have already wired with a wallet. -## Example: deploy the Notary Service and the JWKS root list +## Example: deploy the Notary Service and the Google JWT root list ```rust,no_run use alloy::{primitives::{Address, U256}, providers::ProviderBuilder}; use libid_contracts::{ - bindings::{ceremony::NotaryService, identity::IdentityJwksRoots}, + bindings::ceremony::{GoogleJwtRoots, NotaryService}, deploy::deploy_behind_proxy, Artifacts, }; @@ -51,13 +51,13 @@ async fn main() -> Result<(), Box> { ) .await?; - // The JWKS root list pays that fee on every rotation. It starts empty: + // The Google JWT root list pays that fee on every rotation. It starts empty: // a keeper submits a notarized reading of Google's JWKS to seed it. let roots = deploy_behind_proxy( &provider, &artifacts, - "IdentityJwksRoots", - &IdentityJwksRoots::initializeCall { + "GoogleJwtRoots", + &GoogleJwtRoots::initializeCall { owner_: owner, notary_: notary, }, @@ -65,7 +65,7 @@ async fn main() -> Result<(), Box> { ) .await?; - let fee = IdentityJwksRoots::new(roots, &provider) + let fee = GoogleJwtRoots::new(roots, &provider) .quoteRotation() .call() .await?; diff --git a/rust/contracts/src/artifacts.rs b/rust/contracts/src/artifacts.rs index 65c76347..d07ea8b3 100644 --- a/rust/contracts/src/artifacts.rs +++ b/rust/contracts/src/artifacts.rs @@ -38,9 +38,9 @@ pub const COVERED: &[(&str, &str)] = &[ ("NotaryService", "NotaryService"), ("CeremonyProofVerifier", "CeremonyProofVerifier"), ("ERC1967Proxy", "ERC1967Proxy"), + ("GoogleJwtRoots", "GoogleJwtRoots"), // identity ("IdentityNames", "IdentityNames"), - ("IdentityJwksRoots", "IdentityJwksRoots"), // factory ("LibidFactory", "LibidFactory"), ("WTIA9", "WTIA9"), diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index f159d0d7..3175a198 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -1,7 +1,7 @@ //! Bindings for the ceremony verification path (`solidity/contracts/ceremony/`): -//! the Notary Service every notarized session is authenticated through, and -//! the Proof Verifier that routes a claim to the Platform Verifier registered -//! for its version. +//! 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. /// Bindings for `ceremony/NotaryService.sol` (which implements /// `INotaryService`). @@ -90,3 +90,68 @@ mod proof_verifier_inner { } pub use proof_verifier_inner::CeremonyProofVerifier; + +/// Bindings for `ceremony/GoogleJwtRoots.sol` — the signing keys the +/// `google/v1` Platform Verifier trusts, and until when. Starts EMPTY: +/// Google names bind only once a notarized reading of Google's JWKS has +/// landed here. +/// +/// A rotation is an ordinary notarized session: the keeper reveals the whole +/// `GET /oauth2/v3/certs` exchange, `rotate` hands the attested bytes and the +/// notary's proof to the Notary Service with the Notary Fee attached (read +/// it with `quoteRotation`, which is the service's `fee()`), and the contract +/// reads the JWKS out of the transcript it vouched for. +#[allow(clippy::too_many_arguments, unused_attributes)] +mod google_jwt_roots_inner { + use alloy::sol; + + sol! { + #[sol(rpc)] + interface GoogleJwtRoots { + #[derive(Debug, serde::Serialize, serde::Deserialize)] + struct RootInfo { + bytes32 kidHash; + bytes32 modulusHash; + uint256 observedAt; + uint256 expiresAt; + } + + function initialize(address owner_, address notary_) external; + /// The Notary Service a rotation is verified through. + function notaryService() external view returns (address); + function setNotaryService(address notary_) external; + /// What one rotation costs beyond gas: the Notary Fee, forwarded + /// whole. `rotate` must be sent with exactly this value. + function quoteRotation() external view returns (uint256); + /// Permissionless. `attestedData` is the ceremony-common section + /// 9.1 record of the JWKS session, `proof` the notary's + /// authentication of it (a 65-byte EIP-191 signature today). + function rotate(bytes calldata attestedData, bytes calldata proof) external payable; + function untrustModulus(bytes32 modulusHash) external; + function prune() external; + + function modulusOfKid(bytes32 kidHash) external view returns (bytes32); + function expiresAtKid(bytes32 kidHash) external view returns (uint256); + /// What `GooglePlatformVerifier` reads: modulus hash -> expiry, + /// zero when untrusted. + function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); + function rotatedAtKid(bytes32 kidHash) external view returns (uint256); + function freshestObservedAt() external view returns (uint256); + function currentRoots() external view returns (RootInfo[] memory); + function needsRotation() external view returns (bool); + + function owner() external view returns (address); + function pendingOwner() external view returns (address); + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + event NotaryServiceChanged(address notary); + event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); + event ModulusUntrusted(bytes32 indexed modulusHash); + event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); + event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); + } + } +} + +pub use google_jwt_roots_inner::GoogleJwtRoots; diff --git a/rust/contracts/src/bindings/identity.rs b/rust/contracts/src/bindings/identity.rs index 06d8f056..d669f847 100644 --- a/rust/contracts/src/bindings/identity.rs +++ b/rust/contracts/src/bindings/identity.rs @@ -1,5 +1,5 @@ //! Bindings for the identity-names stack (`solidity/contracts/identity/`): -//! `IdentityNames` and its Google JWKS trust list. +//! `IdentityNames`. /// Bindings for `identity/IdentityNames.sol`. /// @@ -83,67 +83,3 @@ mod names_inner { } pub use names_inner::IdentityNames; - -/// Bindings for `identity/IdentityJwksRoots.sol` — the naming system's own -/// Google JWKS trust list. Starts EMPTY: Google names bind only once a -/// notarized reading of Google's JWKS has landed here. -/// -/// A rotation is an ordinary notarized session: the keeper reveals the whole -/// `GET /oauth2/v3/certs` exchange, `rotate` hands the attested bytes and the -/// notary's proof to the Notary Service with the Notary Fee attached (read -/// it with `quoteRotation`, which is the service's `fee()`), and the contract -/// reads the JWKS out of the transcript it vouched for. -#[allow(clippy::too_many_arguments, unused_attributes)] -mod jwks_roots_inner { - use alloy::sol; - - sol! { - #[sol(rpc)] - interface IdentityJwksRoots { - #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct RootInfo { - bytes32 kidHash; - bytes32 modulusHash; - uint256 observedAt; - uint256 expiresAt; - } - - function initialize(address owner_, address notary_) external; - /// The Notary Service a rotation is verified through. - function notaryService() external view returns (address); - function setNotaryService(address notary_) external; - /// What one rotation costs beyond gas: the Notary Fee, forwarded - /// whole. `rotate` must be sent with exactly this value. - function quoteRotation() external view returns (uint256); - /// Permissionless. `attestedData` is the ceremony-common section - /// 9.1 record of the JWKS session, `proof` the notary's - /// authentication of it (a 65-byte EIP-191 signature today). - function rotate(bytes calldata attestedData, bytes calldata proof) external payable; - function untrustModulus(bytes32 modulusHash) external; - function prune() external; - - function modulusOfKid(bytes32 kidHash) external view returns (bytes32); - function expiresAtKid(bytes32 kidHash) external view returns (uint256); - /// What `GooglePlatformVerifier` reads: modulus hash -> expiry, - /// zero when untrusted. - function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); - function rotatedAtKid(bytes32 kidHash) external view returns (uint256); - function freshestObservedAt() external view returns (uint256); - function currentRoots() external view returns (RootInfo[] memory); - function needsRotation() external view returns (bool); - - function owner() external view returns (address); - function pendingOwner() external view returns (address); - function transferOwnership(address newOwner) external; - function acceptOwnership() external; - - event NotaryServiceChanged(address notary); - event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); - event ModulusUntrusted(bytes32 indexed modulusHash); - event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); - } - } -} - -pub use jwks_roots_inner::IdentityJwksRoots; diff --git a/rust/contracts/src/lib.rs b/rust/contracts/src/lib.rs index 597439f4..a4c41969 100644 --- a/rust/contracts/src/lib.rs +++ b/rust/contracts/src/lib.rs @@ -5,10 +5,10 @@ //! //! - [`bindings`] — hand-written `alloy::sol!` interfaces for every contract a //! consumer talks to: the ceremony verification path (`NotaryService`, -//! `CeremonyProofVerifier`), the naming system (`IdentityNames`) with its -//! Google JWKS trust list (`IdentityJwksRoots`), and the deterministic -//! factory. Kept in lockstep with the Solidity sources in -//! `solidity/contracts`. +//! `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`. //! - [`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/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index dbac798b..039c88c9 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -17,12 +17,10 @@ use libid_contracts::{ bindings::{ ceremony::{ CeremonyProofVerifier, + GoogleJwtRoots, NotaryService, }, - identity::{ - IdentityJwksRoots, - IdentityNames, - }, + identity::IdentityNames, }, deploy::{ deploy_behind_proxy, @@ -42,8 +40,8 @@ async fn default_signer(provider: &impl Provider) -> Address { /// (a) The identity stack in the order `script/Deploy.s.sol` uses: the /// Notary Service first, then the Proof Verifier, the naming system given a -/// keyspace and pointed at the Proof Verifier, and the JWKS root list pointed -/// at the Notary Service — every one behind an ERC1967 proxy. Then the views +/// keyspace and pointed at the Proof Verifier, and the Google JWT root list +/// pointed at the Notary Service — every one behind an ERC1967 proxy. Then the views /// that prove the wiring took. #[tokio::test] async fn deploys_the_identity_stack_behind_proxies() { @@ -90,8 +88,8 @@ async fn deploys_the_identity_stack_behind_proxies() { let roots_proxy = deploy_behind_proxy( &provider, &artifacts, - "IdentityJwksRoots", - &IdentityJwksRoots::initializeCall { + "GoogleJwtRoots", + &GoogleJwtRoots::initializeCall { owner_: deployer, notary_: notary_proxy, }, @@ -157,7 +155,7 @@ async fn deploys_the_identity_stack_behind_proxies() { // The root list points at the Notary Service, quotes its fee, and starts // empty — so it wants a rotation before any Google name can bind. - let roots = IdentityJwksRoots::new(roots_proxy, &provider); + let roots = GoogleJwtRoots::new(roots_proxy, &provider); assert_eq!(roots.notaryService().call().await.unwrap(), notary_proxy); assert_eq!(roots.quoteRotation().call().await.unwrap(), fee); assert!(roots.needsRotation().call().await.unwrap()); diff --git a/scripts/vendor-artifacts.sh b/scripts/vendor-artifacts.sh index 5ce31e9d..0ec69342 100755 --- a/scripts/vendor-artifacts.sh +++ b/scripts/vendor-artifacts.sh @@ -32,9 +32,9 @@ ARTIFACTS=( "NotaryService:NotaryService" "CeremonyProofVerifier:CeremonyProofVerifier" "ERC1967Proxy:ERC1967Proxy" + "GoogleJwtRoots:GoogleJwtRoots" # identity "IdentityNames:IdentityNames" - "IdentityJwksRoots:IdentityJwksRoots" # factory "LibidFactory:LibidFactory" "WTIA9:WTIA9" diff --git a/solidity/contracts/identity/IdentityJwksRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol similarity index 97% rename from solidity/contracts/identity/IdentityJwksRoots.sol rename to solidity/contracts/ceremony/GoogleJwtRoots.sol index 81508a17..29ad90f0 100644 --- a/solidity/contracts/identity/IdentityJwksRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -5,15 +5,16 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; -import {CeremonyAttestation} from "../ceremony/CeremonyAttestation.sol"; -import {INotaryService} from "../ceremony/INotaryService.sol"; +import {CeremonyAttestation} from "./CeremonyAttestation.sol"; +import {INotaryService} from "./INotaryService.sol"; -/// @title IdentityJwksRoots - which Google signing keys the naming system -/// trusts, and until when. +/// @title GoogleJwtRoots - the signing keys the `google/v1` profile trusts, +/// and until when. /// /// @notice Google rotates the keys it signs id_tokens with. This contract is -/// where the naming system learns the current ones, from a notarized -/// reading of Google's published JWKS. +/// where the Google Platform Verifier learns the current ones, from a +/// notarized reading of Google's published JWKS: a JWT verifies only +/// while the modulus that signed it is listed here and unexpired. /// /// @dev The reading is an ordinary notarized session -- the section 9.1 /// record every Platform Verifier consumes, authenticated by the same @@ -49,11 +50,11 @@ import {INotaryService} from "../ceremony/INotaryService.sol"; /// stamp, verifiers check the stamp at use-site, and an expired key stops /// being trusted with no transaction from anyone. The owner's only lever /// over the key list is `untrustModulus`, which can only REMOVE trust. -contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { +contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { // ─── State ────────────────────────────────────────────────────── - /// @custom:storage-location erc7201:libid.storage.IdentityJwksRoots - struct IdentityJwksRootsStorage { + /// @custom:storage-location erc7201:libid.storage.GoogleJwtRoots + struct GoogleJwtRootsStorage { /// Authenticates a reading and charges for it. INotaryService notary; /// kid keccak -> the limb-keccak the circuit produces. @@ -80,13 +81,13 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad uint256 freshestObservedAt; } - // keccak256(abi.encode(uint256(keccak256("libid.storage.IdentityJwksRoots")) - 1)) & ~bytes32(uint256(0xff)) - bytes32 private constant IDENTITY_JWKS_ROOTS_STORAGE = - 0x1aceb787a5cb65251c62e0afbe6fbce480e0d1c9636ff7b90cdca2969527ef00; + // keccak256(abi.encode(uint256(keccak256("libid.storage.GoogleJwtRoots")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant GOOGLE_JWT_ROOTS_STORAGE = + 0x7f78ff13201a03086d4b08e3085224c34a9fc247d0f67d11acd0db52976eb300; - function _s() private pure returns (IdentityJwksRootsStorage storage $) { + function _s() private pure returns (GoogleJwtRootsStorage storage $) { assembly { - $.slot := IDENTITY_JWKS_ROOTS_STORAGE + $.slot := GOOGLE_JWT_ROOTS_STORAGE } } @@ -252,7 +253,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// @param attestedData The exact bytes of ceremony-common section 9.1. /// @param proof The notary's authentication of them, opaque here. function rotate(bytes calldata attestedData, bytes calldata proof) external payable { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); // Exact value, so there is no refund path to get wrong and no value // can be captured in transit. @@ -348,7 +349,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// included until someone prunes. One call tells a keeper the whole /// state of the list. function currentRoots() external view returns (RootInfo[] memory infos) { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); uint256 n = $.trackedKids.length; infos = new RootInfo[](n); for (uint256 i = 0; i < n; ++i) { @@ -366,7 +367,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// the single bit a keeper polls to decide whether to fetch a fresh /// reading. True from deployment until the first rotation lands. function needsRotation() external view returns (bool) { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); uint256 horizon = block.timestamp + RENEWAL_MARGIN; uint256 n = $.trackedKids.length; for (uint256 i = 0; i < n; ++i) { @@ -659,7 +660,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad } function _processClaim(bytes memory kid, bytes32 modulusHash, uint256 expiry, uint256 provenAt) private { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); bytes32 kidHash = keccak256(kid); // Ignore a claim proved no later than the one already applied -- the // monotonic rule that makes replay harmless. Rotation is open and a @@ -674,7 +675,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad // re-stamps nothing and emits nothing, and a second reading dated the // same second cannot swap the kid's key and retire the one Google // still signs with. That is the rule for equal evidence everywhere in - // the naming system (REQ-COMMON-25A). + // the protocol (REQ-COMMON-25A). if (provenAt <= $.rotatedAtKid[kidHash]) return; bytes32 previous = $.modulusOfKid[kidHash]; $.rotatedAtKid[kidHash] = provenAt; @@ -707,7 +708,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// cannot invent one), and the set is capped -- when full, expired entries /// are pruned to make room, and only a set full of LIVE keys refuses. function _trackKid(bytes32 kidHash) private { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); if ($.trackedKidIndex[kidHash] != 0) return; if ($.trackedKids.length >= MAX_TRACKED_KIDS) { _pruneExpired(); @@ -721,7 +722,7 @@ contract IdentityJwksRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgrad /// the monotonic floor that keeps a replayed old reading from resurrecting /// the entry the prune just removed. function _pruneExpired() private { - IdentityJwksRootsStorage storage $ = _s(); + GoogleJwtRootsStorage storage $ = _s(); uint256 i = $.trackedKids.length; while (i > 0) { i--; diff --git a/solidity/contracts/ceremony/GooglePlatformVerifier.sol b/solidity/contracts/ceremony/GooglePlatformVerifier.sol index 29a1b889..02c01f8c 100644 --- a/solidity/contracts/ceremony/GooglePlatformVerifier.sol +++ b/solidity/contracts/ceremony/GooglePlatformVerifier.sol @@ -7,8 +7,9 @@ import {INotaryService} from "./INotaryService.sol"; import {IPlatformVerifier} from "./IPlatformVerifier.sol"; import {IHonkVerifier, PlatformVerifierBase} from "./PlatformVerifierBase.sol"; -/// @dev Where the deployment keeps the Google signing keys it trusts. -interface IJwksRoots { +/// @dev Where the deployment keeps the Google signing keys it trusts: +/// `GoogleJwtRoots`, kept beside this verifier. +interface IGoogleJwtRoots { function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); } @@ -84,7 +85,7 @@ contract GooglePlatformVerifier is IPlatformVerifier, PlatformVerifierBase { /// @custom:storage-location erc7201:libid.storage.GooglePlatformVerifier struct GoogleStorage { - IJwksRoots jwksRoots; + IGoogleJwtRoots jwtRoots; } // keccak256(abi.encode(uint256(keccak256("libid.storage.GooglePlatformVerifier")) - 1)) & ~bytes32(uint256(0xff)) @@ -96,7 +97,7 @@ contract GooglePlatformVerifier is IPlatformVerifier, PlatformVerifierBase { } } - event JwksRootsChanged(address roots); + event JwtRootsChanged(address roots); error WrongPublicInputCount(uint256 expected, uint256 provided); error DigestMismatch(bytes32 proved, bytes32 recomputed); @@ -130,7 +131,7 @@ contract GooglePlatformVerifier is IPlatformVerifier, PlatformVerifierBase { IHonkVerifier honkVerifier_, bytes32 honkVerifierCodehash_, uint64 futureObservationAllowance_, - IJwksRoots jwksRoots_ + IGoogleJwtRoots jwtRoots_ ) external initializer { // No attestation, so no lifetime and no attestation skew: the signed // `exp` is the whole validity ceiling. The allowance is real though -- @@ -140,23 +141,23 @@ contract GooglePlatformVerifier is IPlatformVerifier, PlatformVerifierBase { __PlatformVerifierBase_init( owner_, notary_, honkVerifier_, honkVerifierCodehash_, 0, 0, futureObservationAllowance_ ); - _setJwksRoots(jwksRoots_); + _setJwtRoots(jwtRoots_); } - function jwksRoots() external view returns (address) { - return address(_g().jwksRoots); + function jwtRoots() external view returns (address) { + return address(_g().jwtRoots); } /// @dev Google rotates signing keys weekly, and every ceremony fails closed /// while an active modulus is untrusted (REQ-PLAT-24). - function setJwksRoots(IJwksRoots roots) external onlyOwner { - _setJwksRoots(roots); + function setJwtRoots(IGoogleJwtRoots roots) external onlyOwner { + _setJwtRoots(roots); } - function _setJwksRoots(IJwksRoots roots) private { + function _setJwtRoots(IGoogleJwtRoots roots) private { if (address(roots) == address(0)) revert ZeroAddress(); - _g().jwksRoots = roots; - emit JwksRootsChanged(address(roots)); + _g().jwtRoots = roots; + emit JwtRootsChanged(address(roots)); } function _platform() internal pure override returns (bytes32) { @@ -314,7 +315,7 @@ contract GooglePlatformVerifier is IPlatformVerifier, PlatformVerifierBase { } } bytes32 modulusHash = keccak256(packed); - uint256 expiresAt = _g().jwksRoots.trustedHashExpiresAt(modulusHash); + uint256 expiresAt = _g().jwtRoots.trustedHashExpiresAt(modulusHash); if (expiresAt == 0 || block.timestamp >= expiresAt) revert UntrustedModulus(modulusHash); } } diff --git a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol similarity index 88% rename from solidity/contracts/identity/test/IdentityJwksRoots.t.sol rename to solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index 41fb2100..cd26bc60 100644 --- a/solidity/contracts/identity/test/IdentityJwksRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -5,13 +5,13 @@ import {Test, Vm} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AttestationBuilder} from "../../ceremony/test/AttestationBuilder.sol"; -import {CeremonyAttestation} from "../../ceremony/CeremonyAttestation.sol"; -import {INotaryService} from "../../ceremony/INotaryService.sol"; -import {NotaryService} from "../../ceremony/NotaryService.sol"; -import {IdentityJwksRoots} from "../IdentityJwksRoots.sol"; +import {AttestationBuilder} from "./AttestationBuilder.sol"; +import {CeremonyAttestation} from "../CeremonyAttestation.sol"; +import {INotaryService} from "../INotaryService.sol"; +import {NotaryService} from "../NotaryService.sol"; +import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; -/// The naming system's Google trust list, fed by a notarized reading of +/// The `google/v1` profile's trust list, fed by a notarized reading of /// Google's JWKS endpoint. /// /// The reading is an ordinary section 9.1 session: built with the same @@ -20,8 +20,8 @@ import {IdentityJwksRoots} from "../IdentityJwksRoots.sol"; /// real fee. Nothing here mocks the thing under test. Google's real body rides /// along as a fixture, so the parser is tested against the document it will /// read rather than a shape a test author remembered. -contract IdentityJwksRootsTest is Test { - IdentityJwksRoots roots; +contract GoogleJwtRootsTest is Test { + GoogleJwtRoots roots; NotaryService notary; address constant OWNER = address(0xA11CE); @@ -33,7 +33,7 @@ contract IdentityJwksRootsTest is Test { bytes32 constant AUTHORITY = keccak256("www.googleapis.com"); /// Google's real body, fetched 2026-09-03: pretty-printed, two keys. - string constant GOOGLE_JWKS = "contracts/identity/test/fixtures/google-jwks.json"; + string constant GOOGLE_JWKS = "contracts/ceremony/test/fixtures/google-jwks.json"; bytes32 constant GOOGLE_KID_1 = keccak256("943a3a5d7d919625a454e489b75c29adab57acba"); bytes32 constant GOOGLE_KID_2 = keccak256("f10f87405a979c1df36df26606734f33cd85c271"); @@ -68,10 +68,10 @@ contract IdentityJwksRootsTest is Test { vm.deal(keeper, 1 ether); } - function _deployRoots(INotaryService service) private returns (IdentityJwksRoots) { - IdentityJwksRoots impl = new IdentityJwksRoots(); - return IdentityJwksRoots( - address(new ERC1967Proxy(address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, service)))) + function _deployRoots(INotaryService service) private returns (GoogleJwtRoots) { + GoogleJwtRoots impl = new GoogleJwtRoots(); + return GoogleJwtRoots( + address(new ERC1967Proxy(address(impl), abi.encodeCall(GoogleJwtRoots.initialize, (OWNER, service)))) ); } @@ -392,7 +392,7 @@ contract IdentityJwksRootsTest is Test { vm.warp(block.timestamp + 1 minutes); vm.expectEmit(true, false, false, true); - emit IdentityJwksRoots.ModulusUntrusted(first); + emit GoogleJwtRoots.ModulusUntrusted(first); bytes32 second = _installKid("kid-1", "modulus-two"); assertGt(roots.trustedHashExpiresAt(second), 0, "second key trusted"); @@ -407,7 +407,7 @@ contract IdentityJwksRootsTest is Test { vm.prank(OWNER); vm.expectEmit(true, false, false, true); - emit IdentityJwksRoots.ModulusUntrusted(modulusHash); + emit GoogleJwtRoots.ModulusUntrusted(modulusHash); roots.untrustModulus(modulusHash); assertEq(roots.trustedHashExpiresAt(modulusHash), 0, "no longer trusted"); @@ -433,7 +433,7 @@ contract IdentityJwksRootsTest is Test { vm.warp(expiry); // the stamp is spent; use-site checks already refuse it vm.expectEmit(true, false, false, true); - emit IdentityJwksRoots.RootPruned(kidHash, modulusHash); + emit GoogleJwtRoots.RootPruned(kidHash, modulusHash); vm.prank(keeper); roots.prune(); @@ -468,7 +468,7 @@ contract IdentityJwksRootsTest is Test { assertEq(roots.currentRoots().length, max, "set should sit at the cap"); // A brand-new kid while every slot holds a live key: refused. - _refuses(_reading(_oneKey("kid-overflow", "overflow")), IdentityJwksRoots.TooManyKids.selector); + _refuses(_reading(_oneKey("kid-overflow", "overflow")), GoogleJwtRoots.TooManyKids.selector); // Once the old stamps are spent, the same insert prunes its own room. vm.warp(block.timestamp + roots.DEFAULT_MODULUS_TTL() + 1); @@ -485,7 +485,7 @@ contract IdentityJwksRootsTest is Test { uint64 provenAt = _now() - 5 minutes; _rotate(_readingAt(_manyKeys(3), provenAt)); - IdentityJwksRoots.RootInfo[] memory infos = roots.currentRoots(); + GoogleJwtRoots.RootInfo[] memory infos = roots.currentRoots(); assertEq(infos.length, 3, "every key should be listed"); for (uint256 i = 0; i < infos.length; ++i) { assertEq(infos[i].kidHash, keccak256(bytes(string.concat("kid-", vm.toString(i))))); @@ -551,7 +551,7 @@ contract IdentityJwksRootsTest is Test { vm.prank(OWNER); vm.expectEmit(false, false, false, true); - emit IdentityJwksRoots.NotaryServiceChanged(address(other)); + emit GoogleJwtRoots.NotaryServiceChanged(address(other)); roots.setNotaryService(INotaryService(address(other))); assertEq(roots.notaryService(), address(other)); @@ -577,18 +577,16 @@ contract IdentityJwksRootsTest is Test { function test_aZeroNotaryServiceIsRefused() public { vm.prank(OWNER); - vm.expectRevert(IdentityJwksRoots.ZeroAddress.selector); + vm.expectRevert(GoogleJwtRoots.ZeroAddress.selector); roots.setNotaryService(INotaryService(address(0))); - IdentityJwksRoots impl = new IdentityJwksRoots(); - vm.expectRevert(IdentityJwksRoots.ZeroAddress.selector); - new ERC1967Proxy( - address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, INotaryService(address(0)))) - ); + GoogleJwtRoots impl = new GoogleJwtRoots(); + vm.expectRevert(GoogleJwtRoots.ZeroAddress.selector); + new ERC1967Proxy(address(impl), abi.encodeCall(GoogleJwtRoots.initialize, (OWNER, INotaryService(address(0))))); } /// Renouncing would freeze the list while Google's keys keep expiring, so - /// the naming system would stop accepting Google proofs with no way back. + /// the Google verifier would stop accepting proofs with no way back. function test_renouncingIsDisabled() public { vm.prank(OWNER); vm.expectRevert(bytes("renounce disabled")); @@ -603,13 +601,13 @@ contract IdentityJwksRootsTest is Test { bytes memory attested = _reading(_oneKey("kid-1", "one")); bytes memory sig = _sign(attested); - vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, FEE - 1)); + vm.expectRevert(abi.encodeWithSelector(GoogleJwtRoots.WrongValue.selector, FEE, FEE - 1)); roots.rotate{value: FEE - 1}(attested, sig); - vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, FEE + 1)); + vm.expectRevert(abi.encodeWithSelector(GoogleJwtRoots.WrongValue.selector, FEE, FEE + 1)); roots.rotate{value: FEE + 1}(attested, sig); - vm.expectRevert(abi.encodeWithSelector(IdentityJwksRoots.WrongValue.selector, FEE, 0)); + vm.expectRevert(abi.encodeWithSelector(GoogleJwtRoots.WrongValue.selector, FEE, 0)); roots.rotate(attested, sig); } @@ -660,12 +658,12 @@ contract IdentityJwksRootsTest is Test { bytes32 other = keccak256("storage.googleapis.com"); bytes memory attested = AttestationBuilder.encode(other, _now(), _whole(REQUEST), _whole(_contentLength(_oneKey("kid-1", "one")))); - _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.WrongAuthority.selector, AUTHORITY, other)); + _refuses(attested, abi.encodeWithSelector(GoogleJwtRoots.WrongAuthority.selector, AUTHORITY, other)); } function test_refusesAReadingFromTheFuture() public { uint64 ahead = _now() + uint64(roots.CLOCK_SKEW_GRACE()) + 1; - _refuses(_readingAt(_oneKey("kid-1", "one"), ahead), IdentityJwksRoots.FutureProof.selector); + _refuses(_readingAt(_oneKey("kid-1", "one"), ahead), GoogleJwtRoots.FutureProof.selector); // And accepts one exactly at the grace, which is the boundary. _rotate(_readingAt(_oneKey("kid-1", "one"), ahead - 1)); } @@ -676,7 +674,7 @@ contract IdentityJwksRootsTest is Test { uint64 provenAt = _now(); bytes memory attested = _readingAt(_oneKey("kid-1", "one"), provenAt); vm.warp(provenAt + roots.FRESHNESS_WINDOW() + 1); - _refuses(attested, IdentityJwksRoots.StaleProof.selector); + _refuses(attested, GoogleJwtRoots.StaleProof.selector); } /// Both directions must tile the signed length. A gap is where a prover @@ -718,7 +716,7 @@ contract IdentityJwksRootsTest is Test { }); bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), sent, _whole(_contentLength(_oneKey("kid-1", "one")))); - _refuses(attested, IdentityJwksRoots.HiddenBytes.selector); + _refuses(attested, GoogleJwtRoots.HiddenBytes.selector); } function test_refusesAHiddenByteInTheResponse() public { @@ -732,7 +730,7 @@ contract IdentityJwksRootsTest is Test { length: len + 4 }); bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _whole(REQUEST), received); - _refuses(attested, IdentityJwksRoots.HiddenBytes.selector); + _refuses(attested, GoogleJwtRoots.HiddenBytes.selector); } /// An empty direction passes coverage against a zero signed length; it is @@ -740,12 +738,12 @@ contract IdentityJwksRootsTest is Test { function test_refusesAnEmptyRequest() public { bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _empty(), _whole(_contentLength(_oneKey("kid-1", "one")))); - _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); + _refuses(attested, abi.encodeWithSelector(GoogleJwtRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); } function test_refusesAnEmptyResponse() public { bytes memory attested = AttestationBuilder.encode(AUTHORITY, _now(), _whole(REQUEST), _empty()); - _refuses(attested, abi.encodeWithSelector(IdentityJwksRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); + _refuses(attested, abi.encodeWithSelector(GoogleJwtRoots.RequestLineNotAtOrigin.selector, type(uint32).max)); } // ─── What it refuses: the request ─────────────────────────────── @@ -757,14 +755,14 @@ contract IdentityJwksRootsTest is Test { function test_refusesAPostRequestLine() public { bytes memory request = abi.encodePacked("POST /oauth2/v3/certs HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length)); - _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongRequestLine.selector); } /// The path separates the key set from everything else the host serves. function test_refusesAnotherPath() public { bytes memory request = abi.encodePacked("GET /oauth2/v3/certs2 HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length)); - _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongRequestLine.selector); } /// Only the origin-form line, like every other verifier. @@ -772,7 +770,7 @@ contract IdentityJwksRootsTest is Test { bytes memory request = abi.encodePacked( "GET https://www.googleapis.com/oauth2/v3/certs HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length) ); - _refuses(_withRequest(request), IdentityJwksRoots.WrongRequestLine.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongRequestLine.selector); } /// googleapis.com answers for many hosts under one certificate. With two @@ -784,7 +782,7 @@ contract IdentityJwksRootsTest is Test { "host: storage.googleapis.com\r\n", "connection: close\r\n\r\n" ); - _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 2)); + _refuses(_withRequest(request), abi.encodeWithSelector(GoogleJwtRoots.NotOneHostHeader.selector, 2)); } /// Field names are case-insensitive and the colon admits whitespace, so @@ -796,7 +794,7 @@ contract IdentityJwksRootsTest is Test { "HOST\t : storage.googleapis.com\r\n", "connection: close\r\n\r\n" ); - _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 2)); + _refuses(_withRequest(request), abi.encodeWithSelector(GoogleJwtRoots.NotOneHostHeader.selector, 2)); } /// The notary vouches for the bytes, not their HTTP shape. Google's front @@ -841,18 +839,18 @@ contract IdentityJwksRootsTest is Test { bytes memory request = abi.encodePacked( "GET /oauth2/v3/certs HTTP/1.1\r\n", "connection: close\r\n\r\n", "host: www.googleapis.com\r\n" ); - _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 0)); + _refuses(_withRequest(request), abi.encodeWithSelector(GoogleJwtRoots.NotOneHostHeader.selector, 0)); } /// A head that never ends has no head to count in. function test_refusesARequestWhoseHeadNeverEnds() public { bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com\r\n"; - _refuses(_withRequest(request), IdentityJwksRoots.NoHeadBoundary.selector); + _refuses(_withRequest(request), GoogleJwtRoots.NoHeadBoundary.selector); } function test_refusesARequestWithoutAHostHeader() public { bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nconnection: close\r\n\r\n"; - _refuses(_withRequest(request), abi.encodeWithSelector(IdentityJwksRoots.NotOneHostHeader.selector, 0)); + _refuses(_withRequest(request), abi.encodeWithSelector(GoogleJwtRoots.NotOneHostHeader.selector, 0)); } /// The TLS authority is the certificate's name; the `Host` header is @@ -860,7 +858,7 @@ contract IdentityJwksRootsTest is Test { function test_refusesAnotherGoogleHost() public { bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: storage.googleapis.com\r\nconnection: close\r\n\r\n"; - _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongHost.selector); } /// A host that is a prefix of the right one, or the right one followed by @@ -868,9 +866,9 @@ contract IdentityJwksRootsTest is Test { function test_refusesAHostThatMerelyContainsTheRightOne() public { bytes memory request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com.evil\r\nconnection: close\r\n\r\n"; - _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongHost.selector); request = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.co\r\nconnection: close\r\n\r\n"; - _refuses(_withRequest(request), IdentityJwksRoots.WrongHost.selector); + _refuses(_withRequest(request), GoogleJwtRoots.WrongHost.selector); } // ─── What it refuses: the response ────────────────────────────── @@ -884,12 +882,12 @@ contract IdentityJwksRootsTest is Test { function test_refusesARedirect() public { bytes memory response = abi.encodePacked("HTTP/1.1 302 Found\r\nlocation: /elsewhere\r\ncontent-length: 0\r\n\r\n"); - _refuses(_withResponse(response), IdentityJwksRoots.WrongStatusLine.selector); + _refuses(_withResponse(response), GoogleJwtRoots.WrongStatusLine.selector); } function test_refusesAResponseWithoutAHeadBoundary() public { bytes memory response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "content-length: 2\r\n", "{}"); - _refuses(_withResponse(response), IdentityJwksRoots.NoHeadBoundary.selector); + _refuses(_withResponse(response), GoogleJwtRoots.NoHeadBoundary.selector); } /// Every deviation from strict chunked framing is one error: a size that @@ -903,41 +901,38 @@ contract IdentityJwksRootsTest is Test { // A size that is not hex. _refuses( _withResponse(abi.encodePacked(head, "zz\r\n", body, "\r\n0\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // A chunk extension. _refuses( _withResponse(abi.encodePacked(head, _hex(body.length), ";ext=1\r\n", body, "\r\n0\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // No terminating zero chunk. - _refuses( - _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n")), IdentityJwksRoots.BadChunkFraming.selector - ); + _refuses(_withResponse(abi.encodePacked(head, sizeLine, body, "\r\n")), GoogleJwtRoots.BadChunkFraming.selector); // Trailers after the zero chunk. _refuses( _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\nx-trailer: 1\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // Nine hex digits. _refuses( _withResponse(abi.encodePacked(head, "000000001\r\n", body, "\r\n0\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // Chunk data not followed by CRLF. _refuses( - _withResponse(abi.encodePacked(head, sizeLine, body, "0\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + _withResponse(abi.encodePacked(head, sizeLine, body, "0\r\n\r\n")), GoogleJwtRoots.BadChunkFraming.selector ); // A size past the end of the body. _refuses( _withResponse(abi.encodePacked(head, "ffff\r\n", body, "\r\n0\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // Bytes after the final CRLF. _refuses( _withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\n\r\n\r\n")), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); // And the strict form of the same body is accepted. _rotate(_withResponse(abi.encodePacked(head, sizeLine, body, "\r\n0\r\n\r\n"))); @@ -950,14 +945,14 @@ contract IdentityJwksRootsTest is Test { ); _refuses( _withResponse(response), - abi.encodeWithSelector(IdentityJwksRoots.ContentLengthMismatch.selector, body.length + 1, body.length) + abi.encodeWithSelector(GoogleJwtRoots.ContentLengthMismatch.selector, body.length + 1, body.length) ); // A value that is not a decimal declares no length any body matches. response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "content-length: 4x\r\n\r\n", body); _refuses( _withResponse(response), - abi.encodeWithSelector(IdentityJwksRoots.ContentLengthMismatch.selector, 4, body.length) + abi.encodeWithSelector(GoogleJwtRoots.ContentLengthMismatch.selector, 4, body.length) ); } @@ -978,12 +973,12 @@ contract IdentityJwksRootsTest is Test { function test_refusesABodyWithoutKeys() public { _refuses( _withBody(abi.encodePacked('{"kyes":[', _jwk("kid-1", _modulus("one")), "]}")), - abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "keys") + abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "keys") ); // Present, but not an array. _refuses( _withBody(abi.encodePacked('{"keys":', _jwk("kid-1", _modulus("one")), "}")), - abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "keys") + abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "keys") ); } @@ -993,45 +988,45 @@ contract IdentityJwksRootsTest is Test { bytes memory body = abi.encodePacked( '{"keys":[', _jwk("kid-1", _modulus("one")), '],"keys":[', _jwk("kid-2", _modulus("two")), "]}" ); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.AmbiguousMember.selector, "keys")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.AmbiguousMember.selector, "keys")); } function test_refusesAKeyWithoutAModulus() public { bytes memory body = _body('{"kty":"RSA","kid":"kid-1","e":"AQAB"}'); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "n")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "n")); } function test_refusesAKeyWithoutAKid() public { bytes memory body = _body(abi.encodePacked('{"kty":"RSA","n":"', _modulus("one"), '","e":"AQAB"}')); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.MissingMember.selector, "kid")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "kid")); } /// Two `kid` members in one object: a duplicate leaves the value /// undefined, and different parsers pick different ones. function test_refusesADuplicateKidMember() public { bytes memory body = _body(abi.encodePacked('{"kid":"kid-1","kid":"kid-2","n":"', _modulus("one"), '"}')); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.AmbiguousMember.selector, "kid")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.AmbiguousMember.selector, "kid")); } function test_refusesAnUnterminatedKid() public { bytes memory body = _body(abi.encodePacked('{"n":"', _modulus("one"), '","kid":"kid-1}')); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "kid")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.UnterminatedMember.selector, "kid")); } function test_refusesAnUnterminatedKeysArray() public { _refuses( _withBody(abi.encodePacked('{"keys":[', _jwk("kid-1", _modulus("one")))), - abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + abi.encodeWithSelector(GoogleJwtRoots.UnterminatedMember.selector, "keys") ); // An element that is not an object. _refuses( _withBody(abi.encodePacked('{"keys":[', _jwk("kid-1", _modulus("one")), ',"x"]}')), - abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + abi.encodeWithSelector(GoogleJwtRoots.UnterminatedMember.selector, "keys") ); // An object with no closing brace. _refuses( _withBody(abi.encodePacked('{"keys":[{"kid":"kid-1","n":"', _modulus("one"), '"')), - abi.encodeWithSelector(IdentityJwksRoots.UnterminatedMember.selector, "keys") + abi.encodeWithSelector(GoogleJwtRoots.UnterminatedMember.selector, "keys") ); } @@ -1040,27 +1035,27 @@ contract IdentityJwksRootsTest is Test { /// carry. function test_refusesAnEscapedValue() public { bytes memory body = _body(abi.encodePacked('{"kid":"kid\\"1","n":"', _modulus("one"), '"}')); - _refuses(_withBody(body), abi.encodeWithSelector(IdentityJwksRoots.EscapedValue.selector, "kid")); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.EscapedValue.selector, "kid")); } function test_refusesANestedObject() public { bytes memory body = _body(abi.encodePacked('{"kid":"kid-1","x5c":["MIIC"],"n":"', _modulus("one"), '"}')); - _refuses(_withBody(body), IdentityJwksRoots.UnexpectedNesting.selector); + _refuses(_withBody(body), GoogleJwtRoots.UnexpectedNesting.selector); body = _body(abi.encodePacked('{"kid":"kid-1","inner":{},"n":"', _modulus("one"), '"}')); - _refuses(_withBody(body), IdentityJwksRoots.UnexpectedNesting.selector); + _refuses(_withBody(body), GoogleJwtRoots.UnexpectedNesting.selector); } /// The cap on tracked kids is also the cap on keys in one reading. function test_refusesSeventeenKeys() public { - _refuses(_withBody(_manyKeys(17)), IdentityJwksRoots.TooManyKids.selector); + _refuses(_withBody(_manyKeys(17)), GoogleJwtRoots.TooManyKids.selector); } function test_refusesTrailingBytes() public { bytes memory body = _oneKey("kid-1", "one"); - _refuses(_withBody(abi.encodePacked(body, "x")), IdentityJwksRoots.TrailingBytes.selector); - _refuses(_withBody(abi.encodePacked(body, "{}")), IdentityJwksRoots.TrailingBytes.selector); + _refuses(_withBody(abi.encodePacked(body, "x")), GoogleJwtRoots.TrailingBytes.selector); + _refuses(_withBody(abi.encodePacked(body, "{}")), GoogleJwtRoots.TrailingBytes.selector); // No closing brace at all. - _refuses(_withBody(_slice(body, 0, body.length - 1)), IdentityJwksRoots.TrailingBytes.selector); + _refuses(_withBody(_slice(body, 0, body.length - 1)), GoogleJwtRoots.TrailingBytes.selector); // But whitespace after it is what Google sends. _rotate(_withBody(abi.encodePacked(body, "\n"))); } @@ -1068,24 +1063,24 @@ contract IdentityJwksRootsTest is Test { /// The circuit takes a 2048-bit modulus and nothing else. function test_refusesAShortModulus() public { bytes memory body = _body(_jwk("kid-1", string(_b64url(_bytes("short", 255))))); - _refuses(_withBody(body), IdentityJwksRoots.InvalidModulusLength.selector); + _refuses(_withBody(body), GoogleJwtRoots.InvalidModulusLength.selector); } function test_refusesANonBase64urlCharacter() public { bytes memory body = _body(_jwk("kid-1", "AAA+")); - _refuses(_withBody(body), IdentityJwksRoots.InvalidB64Char.selector); + _refuses(_withBody(body), GoogleJwtRoots.InvalidB64Char.selector); } function test_refusesAnImpossibleBase64Length() public { bytes memory body = _body(_jwk("kid-1", "AAAAA")); - _refuses(_withBody(body), IdentityJwksRoots.InvalidB64Length.selector); + _refuses(_withBody(body), GoogleJwtRoots.InvalidB64Length.selector); } /// A reading with no keys says nothing about what Google publishes. It /// would pay the fee, move `freshestObservedAt` and emit nothing. function test_refusesAnEmptyKeySet() public { - _refuses(_withBody('{"keys":[]}'), IdentityJwksRoots.EmptyKeySet.selector); - _refuses(_withBody('{"keys": [ ]}'), IdentityJwksRoots.EmptyKeySet.selector); + _refuses(_withBody('{"keys":[]}'), GoogleJwtRoots.EmptyKeySet.selector); + _refuses(_withBody('{"keys": [ ]}'), GoogleJwtRoots.EmptyKeySet.selector); } /// Two readings dated the same second cannot disagree about a kid: the @@ -1114,7 +1109,7 @@ contract IdentityJwksRootsTest is Test { bytes memory length = abi.encodePacked("content-length: ", vm.toString(body.length), "\r\n"); _refuses( _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, length, length, "\r\n", body)), - abi.encodeWithSelector(IdentityJwksRoots.DuplicateFramingHeader.selector, "content-length", 2) + abi.encodeWithSelector(GoogleJwtRoots.DuplicateFramingHeader.selector, "content-length", 2) ); bytes memory chunked = "transfer-encoding: chunked\r\n"; _refuses( @@ -1123,16 +1118,16 @@ contract IdentityJwksRootsTest is Test { STATUS_OK, CONTENT_TYPE, chunked, "Transfer-Encoding: identity\r\n", "\r\n", _chunks(body, 64) ) ), - abi.encodeWithSelector(IdentityJwksRoots.DuplicateFramingHeader.selector, "transfer-encoding", 2) + abi.encodeWithSelector(GoogleJwtRoots.DuplicateFramingHeader.selector, "transfer-encoding", 2) ); _refuses( _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, chunked, length, "\r\n", _chunks(body, 64))), - IdentityJwksRoots.ConflictingFraming.selector + GoogleJwtRoots.ConflictingFraming.selector ); // A coding other than chunked frames a body this contract cannot locate. _refuses( _withResponse(abi.encodePacked(STATUS_OK, CONTENT_TYPE, "transfer-encoding: gzip\r\n", "\r\n", body)), - IdentityJwksRoots.BadChunkFraming.selector + GoogleJwtRoots.BadChunkFraming.selector ); } diff --git a/solidity/contracts/ceremony/test/GooglePlatformVerifier.t.sol b/solidity/contracts/ceremony/test/GooglePlatformVerifier.t.sol index 7118c249..dc8265e4 100644 --- a/solidity/contracts/ceremony/test/GooglePlatformVerifier.t.sol +++ b/solidity/contracts/ceremony/test/GooglePlatformVerifier.t.sol @@ -6,7 +6,7 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s import {CeremonyAuthorization} from "../CeremonyAuthorization.sol"; import {CeremonyProfile} from "../CeremonyProfile.sol"; -import {GooglePlatformVerifier, IJwksRoots} from "../GooglePlatformVerifier.sol"; +import {GooglePlatformVerifier, IGoogleJwtRoots} from "../GooglePlatformVerifier.sol"; import {ICeremony} from "../ICeremony.sol"; import {INotaryService} from "../INotaryService.sol"; import {IHonkVerifier, PlatformVerifierBase} from "../PlatformVerifierBase.sol"; @@ -25,7 +25,7 @@ contract Honk is IHonkVerifier { } } -contract Roots is IJwksRoots { +contract Roots is IGoogleJwtRoots { mapping(bytes32 => uint256) public expiry; function trust(bytes32 h, uint256 until) external { @@ -79,7 +79,7 @@ contract GooglePlatformVerifierTest is Test { IHonkVerifier(address(honk)), address(honk).codehash, GOOGLE_ALLOWANCE, - IJwksRoots(address(roots)) + IGoogleJwtRoots(address(roots)) ) ) ) @@ -225,7 +225,7 @@ contract GooglePlatformVerifierTest is Test { IHonkVerifier(address(honk)), address(honk).codehash, GOOGLE_ALLOWANCE, - IJwksRoots(address(roots)) + IGoogleJwtRoots(address(roots)) ) ) ); @@ -348,7 +348,7 @@ contract GooglePlatformVerifierTest is Test { function test_rejectsAnUntrustedModulus() public { Roots empty = new Roots(); vm.prank(OWNER); - verifier.setJwksRoots(IJwksRoots(address(empty))); + verifier.setJwtRoots(IGoogleJwtRoots(address(empty))); GooglePlatformVerifier.GoogleProof memory s = _payload(); vm.expectPartialRevert(GooglePlatformVerifier.UntrustedModulus.selector); this.run(s); @@ -360,7 +360,7 @@ contract GooglePlatformVerifierTest is Test { Roots lapsed = new Roots(); lapsed.trust(_modulusHash(), T0); vm.prank(OWNER); - verifier.setJwksRoots(IJwksRoots(address(lapsed))); + verifier.setJwtRoots(IGoogleJwtRoots(address(lapsed))); GooglePlatformVerifier.GoogleProof memory s = _payload(); vm.expectPartialRevert(GooglePlatformVerifier.UntrustedModulus.selector); this.run(s); @@ -431,12 +431,12 @@ contract GooglePlatformVerifierTest is Test { this.run(s); } - function test_onlyTheOwnerPointsAtJwksRoots() public { + function test_onlyTheOwnerPointsAtJwtRoots() public { vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this))); - verifier.setJwksRoots(IJwksRoots(address(roots))); + verifier.setJwtRoots(IGoogleJwtRoots(address(roots))); vm.prank(OWNER); vm.expectRevert(PlatformVerifierBase.ZeroAddress.selector); - verifier.setJwksRoots(IJwksRoots(address(0))); + verifier.setJwtRoots(IGoogleJwtRoots(address(0))); } /// @dev A TLSNotary-shaped payload at the Google verifier: word four of diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index 5d6162a6..7835ed40 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -10,13 +10,13 @@ import {CeremonyProofVerifier} from "../CeremonyProofVerifier.sol"; import {NotaryService} from "../NotaryService.sol"; import {XPlatformVerifier} from "../XPlatformVerifier.sol"; import {GitHubPlatformVerifier} from "../GitHubPlatformVerifier.sol"; -import {GooglePlatformVerifier, IJwksRoots} from "../GooglePlatformVerifier.sol"; +import {GooglePlatformVerifier, IGoogleJwtRoots} from "../GooglePlatformVerifier.sol"; import {INotaryService} from "../INotaryService.sol"; import {IHonkVerifier} from "../PlatformVerifierBase.sol"; import {IPlatformVerifier} from "../IPlatformVerifier.sol"; import {IProofVerifier} from "../IProofVerifier.sol"; import {IdentityNames} from "../../identity/IdentityNames.sol"; -import {IdentityJwksRoots} from "../../identity/IdentityJwksRoots.sol"; +import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; import {HandleVectors} from "../../identity/HandleVectors.sol"; import {HandleNormalizer} from "../../identity/HandleNormalizer.sol"; import {IdentityNodes} from "../../identity/IdentityNodes.sol"; @@ -29,7 +29,7 @@ contract RHonk is IHonkVerifier { } } -contract RRoots is IJwksRoots { +contract RRoots is IGoogleJwtRoots { function trustedHashExpiresAt(bytes32) external pure returns (uint256) { return 0; } @@ -67,7 +67,7 @@ contract UpgradeSafetyTest is Test { ); assertEq(_slot("libid.storage.IdentityNames"), NAMES_ROOT); assertEq( - _slot("libid.storage.IdentityJwksRoots"), 0x1aceb787a5cb65251c62e0afbe6fbce480e0d1c9636ff7b90cdca2969527ef00 + _slot("libid.storage.GoogleJwtRoots"), 0x7f78ff13201a03086d4b08e3085224c34a9fc247d0f67d11acd0db52976eb300 ); } @@ -234,7 +234,7 @@ contract UpgradeSafetyTest is Test { vm.prank(OWNER); v.upgradeToAndCall(address(impl2), ""); assertEq(_implOf(address(v)), address(impl2)); - assertEq(v.jwksRoots(), address(roots)); + assertEq(v.jwtRoots(), address(roots)); assertEq(v.honkVerifier(), address(honk)); assertEq(v.notaryService(), address(0)); (,, uint64 c) = v.protocolParameters(); @@ -242,15 +242,15 @@ contract UpgradeSafetyTest is Test { assertEq(v.owner(), OWNER); } - // ─── IdentityJwksRoots ────────────────────────────────────────── + // ─── GoogleJwtRoots ────────────────────────────────────────── - function test_upgrade_IdentityJwksRoots() public { + function test_upgrade_GoogleJwtRoots() public { INotaryService ns = INotaryService(address(_notaryService())); - IdentityJwksRoots impl = new IdentityJwksRoots(); + GoogleJwtRoots impl = new GoogleJwtRoots(); vm.expectRevert(Initializable.InvalidInitialization.selector); impl.initialize(OWNER, ns); - IdentityJwksRoots r = IdentityJwksRoots( - address(new ERC1967Proxy(address(impl), abi.encodeCall(IdentityJwksRoots.initialize, (OWNER, ns)))) + GoogleJwtRoots r = GoogleJwtRoots( + address(new ERC1967Proxy(address(impl), abi.encodeCall(GoogleJwtRoots.initialize, (OWNER, ns)))) ); vm.expectRevert(Initializable.InvalidInitialization.selector); r.initialize(OWNER, ns); @@ -272,7 +272,7 @@ contract UpgradeSafetyTest is Test { uint256 stamp = r.rotatedAtKid(kidHash); uint256 expiry = r.expiresAtKid(kidHash); - IdentityJwksRoots impl2 = new IdentityJwksRoots(); + GoogleJwtRoots impl2 = new GoogleJwtRoots(); _expectNotOwner(); r.upgradeToAndCall(address(impl2), ""); vm.prank(OWNER); diff --git a/solidity/contracts/identity/test/fixtures/google-jwks.json b/solidity/contracts/ceremony/test/fixtures/google-jwks.json similarity index 100% rename from solidity/contracts/identity/test/fixtures/google-jwks.json rename to solidity/contracts/ceremony/test/fixtures/google-jwks.json diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 90f77ae6..33ba7640 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -20,7 +20,7 @@ cbor_metadata = false fs_permissions = [ # Google's real JWKS body, as served today, so the root list's parser is # tested against the document it will actually read rather than a shape - # a test author remembered (see IdentityJwksRoots.t.sol). - { access = "read", path = "contracts/identity/test/fixtures" }, + # a test author remembered (see GoogleJwtRoots.t.sol). + { access = "read", path = "contracts/ceremony/test/fixtures" }, { access = "read", path = "out" }, # the factory tests read creation code from the artifacts ] diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index e97dcf2e..2d470473 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -9,14 +9,14 @@ import {INotaryService} from "../contracts/ceremony/INotaryService.sol"; import {CeremonyProofVerifier} from "../contracts/ceremony/CeremonyProofVerifier.sol"; import {IProofVerifier} from "../contracts/ceremony/IProofVerifier.sol"; import {HandleVectors} from "../contracts/identity/HandleVectors.sol"; -import {IdentityJwksRoots} from "../contracts/identity/IdentityJwksRoots.sol"; +import {GoogleJwtRoots} from "../contracts/ceremony/GoogleJwtRoots.sol"; /// @notice Deploy the identity stack to any EVM chain. /// /// Four UUPS proxies, in dependency order: the Notary Service every notarized /// session is verified through, the Proof Verifier the naming system /// dispatches claims through, the naming system itself with a keyspace per -/// platform, and the Google JWKS root list that pays the Notary Service for +/// platform, and the Google JWT root list that pays the Notary Service for /// each rotation. No Platform Verifier is registered here -- that needs the /// ceremony circuit artifacts, which arrive with their own release. /// @@ -83,16 +83,16 @@ contract Deploy is Script { _wireIdentityPlatform(names, HandleVectors.PLATFORM_GITHUB); _wireIdentityPlatform(names, HandleVectors.PLATFORM_GOOGLE); - // 4. The JWKS root list is the naming system's own. Google's Platform - // Verifier reads the trusted moduli through it, and nothing is - // trusted until a notarized reading of Google's JWKS lands -- + // 4. The Google JWT root list, beside the Platform Verifier it serves. + // That verifier reads the trusted moduli through it, and nothing + // is trusted until a notarized reading of Google's JWKS lands -- // verified through the Notary Service above, like every other // notarized session, and paying the same fee. - IdentityJwksRoots rootsImpl = new IdentityJwksRoots(); - address jwksRootsAddr = address( + GoogleJwtRoots rootsImpl = new GoogleJwtRoots(); + address jwtRootsAddr = address( new ERC1967Proxy( address(rootsImpl), - abi.encodeCall(IdentityJwksRoots.initialize, (deployer, INotaryService(notaryServiceAddr))) + abi.encodeCall(GoogleJwtRoots.initialize, (deployer, INotaryService(notaryServiceAddr))) ) ); @@ -103,7 +103,7 @@ contract Deploy is Script { console.log("NOTARY_SERVICE_ADDRESS= ", notaryServiceAddr); console.log("CEREMONY_PROOF_VERIFIER_ADDRESS= ", proofVerifierAddr); console.log("IDENTITY_NAMES_ADDRESS= ", identityNamesAddr); - console.log("IDENTITY_JWKS_ROOTS_ADDRESS= ", jwksRootsAddr); + console.log("GOOGLE_JWT_ROOTS_ADDRESS= ", jwtRootsAddr); console.log("NOTE: no Platform Verifier is registered yet. Add one with"); console.log(" CeremonyProofVerifier.setVerifier once the ceremony"); console.log(" circuit artifacts are released. Until then a platform"); @@ -111,7 +111,7 @@ contract Deploy is Script { // Nothing is trusted until a notarized reading of Google's JWKS lands. // Until then every Google claim reverts `UntrustedModulus`, which reads // as a bad proof rather than an unseeded list. - console.log("NOTE: point a JWKS rotation listener at IDENTITY_JWKS_ROOTS_ADDRESS"); + console.log("NOTE: point a JWKS rotation listener at GOOGLE_JWT_ROOTS_ADDRESS"); console.log(" before Google names work. The trust list starts empty."); } diff --git a/ts/packages/contracts/README.md b/ts/packages/contracts/README.md index 66ad6190..5f261554 100644 --- a/ts/packages/contracts/README.md +++ b/ts/packages/contracts/README.md @@ -22,7 +22,7 @@ pnpm add @libid/contracts viem ```ts import { createPublicClient, http } from 'viem' -import { identityJwksRootsAbi, identityNamesAbi } from '@libid/contracts/abis' +import { googleJwtRootsAbi, identityNamesAbi } from '@libid/contracts/abis' const client = createPublicClient({ transport: http(RPC_URL) }) @@ -35,8 +35,8 @@ const owner = await client.readContract({ }) const roots = await client.readContract({ - address: IDENTITY_JWKS_ROOTS, - abi: identityJwksRootsAbi, + address: GOOGLE_JWT_ROOTS, + abi: googleJwtRootsAbi, functionName: 'currentRoots', }) ``` @@ -63,7 +63,7 @@ before their arguments and set it on the returned call: ```ts // A JWKS rotation pays the Notary Fee: read it with `quoteRotation` first. -const rotate = calls.identityJwksRoots.rotate(roots, fee, attestedData, proof) +const rotate = calls.googleJwtRoots.rotate(roots, fee, attestedData, proof) // { to: `0x…`, value: fee, data: `0x…` } ``` diff --git a/ts/packages/contracts/scripts/codegen.mjs b/ts/packages/contracts/scripts/codegen.mjs index 17b885bd..b1eff933 100644 --- a/ts/packages/contracts/scripts/codegen.mjs +++ b/ts/packages/contracts/scripts/codegen.mjs @@ -32,13 +32,13 @@ const callsDir = join(packageDir, 'src', 'calls') // imports. const contracts = [ { file: 'IdentityNames.sol', name: 'IdentityNames', exportName: 'identityNamesAbi' }, - { file: 'IdentityJwksRoots.sol', name: 'IdentityJwksRoots', exportName: 'identityJwksRootsAbi' }, { file: 'NotaryService.sol', name: 'NotaryService', exportName: 'notaryServiceAbi' }, { file: 'CeremonyProofVerifier.sol', name: 'CeremonyProofVerifier', exportName: 'ceremonyProofVerifierAbi', }, + { file: 'GoogleJwtRoots.sol', name: 'GoogleJwtRoots', exportName: 'googleJwtRootsAbi' }, { file: 'LibidFactory.sol', name: 'LibidFactory', exportName: 'libidFactoryAbi' }, { file: 'WTIA9.sol', name: 'WTIA9', exportName: 'wtia9Abi' }, ] diff --git a/ts/packages/contracts/src/calls.test.ts b/ts/packages/contracts/src/calls.test.ts index 8c3dafc7..150c5873 100644 --- a/ts/packages/contracts/src/calls.test.ts +++ b/ts/packages/contracts/src/calls.test.ts @@ -1,7 +1,7 @@ import { decodeFunctionData } from 'viem' import { describe, expect, it } from 'vitest' -import { identityJwksRootsAbi } from './abis/identityJwksRoots.js' +import { googleJwtRootsAbi } from './abis/googleJwtRoots.js' import { identityNamesAbi } from './abis/identityNames.js' import { calls } from './index.js' @@ -37,11 +37,11 @@ describe('generated call builders', () => { it('takes value before the arguments on a payable function', () => { // A rotation pays the Notary Fee, so the value is the part a caller must // not be able to forget: it comes before the attested bytes and the proof. - const call = calls.identityJwksRoots.rotate(ROOTS, 7n, ATTESTED, PROOF) + const call = calls.googleJwtRoots.rotate(ROOTS, 7n, ATTESTED, PROOF) expect(call.value).toBe(7n) expect(call.to).toBe(ROOTS) - expect(decodeFunctionData({ abi: identityJwksRootsAbi, data: call.data })).toEqual({ + expect(decodeFunctionData({ abi: googleJwtRootsAbi, data: call.data })).toEqual({ functionName: 'rotate', args: [ATTESTED, PROOF], }) From 12b11f8267cf576f767ea8b8421590ee37737b52 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 11:41:16 +0100 Subject: [PATCH 07/15] test(ceremony): a real notarized reading of Google's JWKS, and the ways to cheat with it Every session GoogleJwtRoots.t.sol feeds the list is one a test author built: a constant request, a response the test framed, a notary that is vm.sign. That proves the parser against shapes we imagined, and nothing against the shape the deployment will actually meet. The fixture added here is a genuine MPC-TLS session with www.googleapis.com, captured on 2026-09-03 by notary/examples/notarize_jwks.rs through a notary signing with anvil #0 -- request line, head, framing, key set and signature all as they crossed the wire, none written by hand. The real bytes and the real signature rotate the list through a real NotaryService trusting that key, and every kid Google published that minute lands: four today, not the two or three the design anticipated, with member order varying inside each JWK. The same body re-read under Content-Length by a second trusted key installs identical modulus hashes on a fresh list, so the framing and the signer are transparent to what gets written. Two facts about the wire are pinned while they are cheap to pin: the request line is origin-form, which is the libid-rs prover fix proven end to end, and Google frames the body chunked with capitalised field names -- the one head the normalisation is tested against that a server wrote rather than a builder. The cheats are derived from the real record rather than invented, each refused by its exact selector and arguments: one flipped bit under the genuine signature recovers to a key nobody trusts (and the test names which), a stranger's signature over the untouched bytes, the record presented one second past its window, and a bare-LF Host smuggled into the real request and re-signed by a key the service does trust. The last one re-encodes through AttestationBuilder, which a test first shows reproduces the real record byte for byte, so the forgery differs from the wire in the edit alone. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .../test/GoogleJwtRootsRealSession.t.sol | 441 ++++++++++++++++++ .../test/fixtures/google-jwks-session.json | 8 + solidity/foundry.toml | 4 +- 3 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol create mode 100644 solidity/contracts/ceremony/test/fixtures/google-jwks-session.json diff --git a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol new file mode 100644 index 00000000..37e63eec --- /dev/null +++ b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol @@ -0,0 +1,441 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import {AttestationBuilder} from "./AttestationBuilder.sol"; +import {CeremonyAttestation} from "../CeremonyAttestation.sol"; +import {INotaryService} from "../INotaryService.sol"; +import {NotaryService} from "../NotaryService.sol"; +import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; + +/// A reading of Google's JWKS that actually crossed the wire. +/// +/// `GoogleJwtRoots.t.sol` builds its sessions: the request is a constant, the +/// response is framed by the test, and the notary is `vm.sign`. Every byte +/// there is one a test author chose. This file starts from the other end: the +/// fixture is a genuine MPC-TLS session with www.googleapis.com, captured on +/// 2026-09-03 by `notary/examples/notarize_jwks.rs` through a notary signing +/// with anvil #0, and nothing in it was written by hand -- the request line is +/// what the keeper's prover put on the wire, the head is what Google's front +/// end answered, the framing is Google's, the key set is the one Google +/// published that minute, and the signature is the notary's own. +/// +/// So the positive test here is the deployment's real path end to end, and +/// the negative ones are what a cheat has to do to a real record: change a +/// byte, borrow a signature, wait too long, or re-sign an edited request with +/// a key the service does trust. +contract GoogleJwtRootsRealSessionTest is Test { + GoogleJwtRoots roots; + NotaryService notary; + + address constant OWNER = address(0xA11CE); + uint256 constant FEE = 0.001 ether; + /// anvil #0: the key the notary that produced the fixture signed with. + uint256 constant ANVIL_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + /// A second key the service trusts, for the variants that re-sign the + /// real bytes. Distinct from anvil #0 so a variant that passes is seen to + /// pass on the strength of the service's trust, not the fixture's. + uint256 constant TEST_KEY = 0x7E57; + uint256 constant STRANGER_KEY = 0xB0B; + + string constant SESSION = "contracts/ceremony/test/fixtures/google-jwks-session.json"; + bytes constant REQUEST_LINE = "GET /oauth2/v3/certs HTTP/1.1\r\n"; + bytes constant HEAD_BOUNDARY = "\r\n\r\n"; + uint256 constant NONE = type(uint256).max; + + /// The record and its signature, exactly as the notary emitted them. + bytes record; + bytes signature; + /// The record's `createdAt`, from the fixture's own field. + uint64 createdAt; + /// Who the fixture says signed. + address signer; + + function setUp() public { + string memory json = vm.readFile(SESSION); + record = vm.parseJsonBytes(json, ".attested_data"); + signature = vm.parseJsonBytes(json, ".notary_signature"); + createdAt = uint64(vm.parseJsonUint(json, ".created_at")); + signer = vm.parseJsonAddress(json, ".notary"); + + // A minute after the notary's clock: inside the freshness window, + // outside the clock-skew grace, the way a keeper's submission lands. + vm.warp(createdAt + 60); + + NotaryService impl = new NotaryService(); + notary = NotaryService( + address( + new ERC1967Proxy( + address(impl), abi.encodeCall(NotaryService.initialize, (OWNER, vm.addr(ANVIL_KEY), FEE)) + ) + ) + ); + vm.prank(OWNER); + notary.setNotary(vm.addr(TEST_KEY), true); + roots = _deployRoots(); + + vm.deal(address(this), 1 ether); + } + + function _deployRoots() private returns (GoogleJwtRoots) { + GoogleJwtRoots impl = new GoogleJwtRoots(); + return GoogleJwtRoots( + address( + new ERC1967Proxy( + address(impl), abi.encodeCall(GoogleJwtRoots.initialize, (OWNER, INotaryService(address(notary)))) + ) + ) + ); + } + + /// The four kids Google served in the fixture. Asserted against the body + /// below, so this list cannot drift from the record it describes. + function _kids() private pure returns (string[] memory kids) { + kids = new string[](4); + kids[0] = "d6a0e659c3392d9e592a4b0ad88e7b2b88076909"; + kids[1] = "a8f80b512469959cdc1eeba44b066c2f79944779"; + kids[2] = "943a3a5d7d919625a454e489b75c29adab57acba"; + kids[3] = "f10f87405a979c1df36df26606734f33cd85c271"; + } + + // ─── What the fixture is ──────────────────────────────────────── + + /// The record is the JWKS session layout, signed by the key the fixture + /// names, and its bytes sit where the section 9.1 layout fixes them: the + /// sent direction's first revealed range begins at 48 + 8 + 4 + 8 = 68, + /// the received one at 68 + 136 + 8 + 8 + 4 + 8 = 232. + function test_theRecordIsTheSessionTheFixtureDescribes() public view { + assertEq(signer, vm.addr(ANVIL_KEY), "the fixture was signed by anvil #0"); + assertEq(record.length, 2811); + assertEq(signature.length, 65); + + bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(keccak256(record)); + assertEq(ECDSA.recover(ethHash, signature), signer, "the signature is over these exact bytes"); + + CeremonyAttestation.AttestedData memory data = this.decode(record); + assertEq(data.authorityId, roots.AUTHORITY(), "the notary authenticated www.googleapis.com"); + assertEq(data.createdAt, createdAt, "bytes 32..40 are the fixture's created_at"); + assertEq(data.sentTranscriptLength, 136); + assertEq(data.recvTranscriptLength, 2571); + + // The JWKS layout: everything revealed, nothing committed, in both + // directions -- what lets the contract read each join as the wire. + assertEq(data.sent.revealed.length, 1); + assertEq(data.sent.revealed[0].start, 0); + assertEq(data.sent.revealed[0].end, 136); + assertEq(data.sent.commitments.length, 0); + assertEq(data.received.revealed.length, 1); + assertEq(data.received.revealed[0].start, 0); + assertEq(data.received.revealed[0].end, 2571); + assertEq(data.received.commitments.length, 0); + + assertEq(_slice(record, 68, 68 + REQUEST_LINE.length), REQUEST_LINE, "the request begins at byte 68"); + assertEq(_slice(record, 232, 232 + 17), bytes("HTTP/1.1 200 OK\r\n"), "the response begins at byte 232"); + } + + /// The libid-rs origin-form fix, proven on the wire: hyper writes the + /// request-target as the URI displays, and the prover was handed an + /// absolute one, so without the rewrite this line would read + /// `GET https://www.googleapis.com/oauth2/v3/certs HTTP/1.1` and every + /// verifier would refuse it. + function test_theRealRequestLineIsOriginForm() public view { + bytes memory sent = this.decode(record).sent.revealed[0].value; + assertTrue(_startsWith(sent, REQUEST_LINE), "origin-form request line"); + assertEq(_indexOf(sent, "://", 0), NONE, "an absolute-form target would carry a scheme"); + + bytes memory rest = _slice(sent, REQUEST_LINE.length, sent.length); + assertTrue(_startsWith(rest, "host: www.googleapis.com\r\n"), "the Host the contract pins, right after it"); + assertEq(_count(sent, "\r\nhost:"), 1, "one Host header"); + assertTrue(_endsWith(sent, HEAD_BOUNDARY), "a head and no body"); + } + + /// Google framed the body chunked, with capitalised field names -- the + /// builder-made sessions write them lowercase, so this is the one place + /// the head normalisation is tested against a head a server wrote. One + /// chunk of 0x7ff bytes, then the terminator, no trailers. + function test_googleFramedTheBodyChunked() public view { + bytes memory received = this.decode(record).received.revealed[0].value; + bytes memory head = _head(received); + bytes memory body = _afterHead(received); + + assertTrue(_startsWith(head, "HTTP/1.1 200 OK\r\n")); + assertTrue(_contains(head, "\r\nTransfer-Encoding: chunked\r\n"), "chunked, capitalised as Google writes it"); + assertTrue(_contains(head, "\r\nContent-Type: application/json; charset=UTF-8\r\n")); + assertTrue(_contains(head, "\r\nConnection: close\r\n")); + + bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(head); + assertEq(_count(normalized, "\r\ntransfer-encoding:chunked\r\n"), 1, "the needle the contract counts"); + assertEq(_count(normalized, "\r\ncontent-length:"), 0, "no length alongside the coding"); + + assertTrue(_startsWith(body, "7ff\r\n"), "one chunk of 2047 bytes"); + assertTrue(_endsWith(body, "\r\n0\r\n\r\n"), "the terminator, no trailers"); + bytes memory json = _dechunk(body); + assertEq(json.length, 0x7ff); + assertTrue(_startsWith(json, "{\n \"keys\": ["), "Google's pretty-printed key set"); + } + + /// The builder used to forge variants below reproduces the real record + /// byte for byte, so a variant differs from the wire in exactly the edit + /// it makes and nothing else. + function test_theBuilderReproducesTheRealRecordByteForByte() public view { + CeremonyAttestation.AttestedData memory data = this.decode(record); + bytes memory rebuilt = AttestationBuilder.encode( + data.authorityId, + data.createdAt, + _whole(data.sent.revealed[0].value), + _whole(data.received.revealed[0].value) + ); + assertEq(rebuilt, record); + } + + // ─── The real rotation ────────────────────────────────────────── + + /// The real bytes, the real signature, a real Notary Service trusting the + /// key that signed: every kid Google published that minute is installed, + /// stamped with the notary's clock, and the fee lands where every other + /// attestation's does. + function test_theRealReadingInstallsEveryKidGoogleServed() public { + roots.rotate{value: FEE}(record, signature); + + bytes memory json = _realBody(); + string[] memory kids = _kids(); + assertEq(_count(json, "\"kid\""), kids.length, "the list above names every kid in the body"); + + bytes32[] memory moduli = new bytes32[](kids.length); + for (uint256 i = 0; i < kids.length; ++i) { + assertTrue(_contains(json, abi.encodePacked("\"kid\": \"", kids[i], "\"")), "kid is in the body"); + bytes32 kidHash = keccak256(bytes(kids[i])); + moduli[i] = roots.modulusOfKid(kidHash); + assertTrue(moduli[i] != bytes32(0), "kid installed"); + assertEq(roots.rotatedAtKid(kidHash), createdAt, "stamped with the notary's clock"); + assertEq(roots.expiresAtKid(kidHash), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + assertEq(roots.trustedHashExpiresAt(moduli[i]), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + for (uint256 j = 0; j < i; ++j) { + assertTrue(moduli[i] != moduli[j], "four distinct keys"); + } + } + + assertEq(roots.currentRoots().length, kids.length); + assertFalse(roots.needsRotation()); + assertEq(roots.freshestObservedAt(), createdAt); + assertEq(address(notary).balance, FEE, "the fee reached the Notary Service"); + assertEq(address(roots).balance, 0); + } + + /// The same body, re-read under `Content-Length` by a different trusted + /// notary, installs identical modulus hashes on a fresh list: the framing + /// and the signer are transparent to what gets written, which is the + /// keeper's own limb hash of the bytes Google sent. + function test_theSameBodyUnderContentLengthYieldsTheSameKeys() public { + roots.rotate{value: FEE}(record, signature); + + CeremonyAttestation.AttestedData memory data = this.decode(record); + bytes memory json = _realBody(); + bytes memory response = abi.encodePacked( + "HTTP/1.1 200 OK\r\n", + "content-type: application/json; charset=UTF-8\r\n", + "content-length: ", + vm.toString(json.length), + "\r\n\r\n", + json + ); + bytes memory reread = AttestationBuilder.encode( + data.authorityId, data.createdAt, _whole(data.sent.revealed[0].value), _whole(response) + ); + + GoogleJwtRoots again = _deployRoots(); + again.rotate{value: FEE}(reread, _signWith(TEST_KEY, reread)); + + string[] memory kids = _kids(); + for (uint256 i = 0; i < kids.length; ++i) { + bytes32 kidHash = keccak256(bytes(kids[i])); + assertEq(again.modulusOfKid(kidHash), roots.modulusOfKid(kidHash), "framing changed the key"); + } + assertEq(again.currentRoots().length, kids.length); + } + + // ─── The ways to cheat with it ────────────────────────────────── + // + // Two kinds of surgery. Where the point is that no byte may change, the + // edit is made on the raw record at the offset the layout fixes, so the + // signature is checked against exactly what the notary signed minus one + // bit. Where the point is that the CONTENT is refused even under a good + // signature, the record is decoded with `CeremonyAttestation`, the + // request bytes are edited, and the result is re-encoded with + // `AttestationBuilder` and re-signed -- legitimate because the builder + // reproduces the real record byte for byte (see above), so the forged + // record differs from the wire in the edit alone. + + /// One bit inside the first modulus. The signature is the notary's and the + /// key is trusted, but the digest the service derives is of these bytes, + /// so recovery lands on a key nobody trusts -- and the test names which. + function test_aFlippedByteIsRefusedThoughTheSignatureIsGenuine() public { + bytes memory tampered = record; + uint256 at = _indexOf(tampered, "\"n\": \"", 0) + 10; + tampered[at] = bytes1(uint8(tampered[at]) ^ 0x01); + + bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(keccak256(tampered)); + (address recovered, ECDSA.RecoverError err,) = ECDSA.tryRecover(ethHash, signature); + assertEq(uint8(err), uint8(ECDSA.RecoverError.NoError), "a well-formed signature still recovers to someone"); + assertTrue(recovered != signer, "just not to the notary"); + + vm.expectRevert(abi.encodeWithSelector(NotaryService.UntrustedNotary.selector, recovered)); + roots.rotate{value: FEE}(tampered, signature); + } + + /// The real bytes, signed by someone the service never trusted. + function test_aStrangersSignatureOverTheRealBytesIsRefused() public { + bytes memory sig = _signWith(STRANGER_KEY, record); + vm.expectRevert(abi.encodeWithSelector(NotaryService.UntrustedNotary.selector, vm.addr(STRANGER_KEY))); + roots.rotate{value: FEE}(record, sig); + } + + /// Genuine, and too old: past the window the reading says nothing about + /// what Google publishes now. The boundary itself is still inside. + function test_theRealReadingIsRefusedPastItsWindow() public { + uint256 window = roots.FRESHNESS_WINDOW(); + + vm.warp(createdAt + window + 1); + vm.expectRevert(GoogleJwtRoots.StaleProof.selector); + roots.rotate{value: FEE}(record, signature); + + vm.warp(createdAt + window); + roots.rotate{value: FEE}(record, signature); + assertEq(roots.currentRoots().length, 4); + } + + /// The real request with a second `Host` smuggled in behind a bare LF -- + /// the line Google's front end honours and a CRLF-anchored count cannot + /// see -- re-signed by a key the service DOES trust. The signature is + /// good; the request is refused before any header is counted, at the + /// offset of the LF: 31 bytes of request line, then `x-a: b`. + function test_aSmuggledHostReSignedByATrustedKeyIsRefused() public { + assertTrue(notary.isTrustedNotary(vm.addr(TEST_KEY)), "the forger holds a trusted key"); + + CeremonyAttestation.AttestedData memory data = this.decode(record); + bytes memory sent = data.sent.revealed[0].value; + bytes memory smuggled = abi.encodePacked( + _slice(sent, 0, REQUEST_LINE.length), + "x-a: b\nhost: storage.googleapis.com\r\n", + _slice(sent, REQUEST_LINE.length, sent.length) + ); + bytes memory forged = AttestationBuilder.encode( + data.authorityId, data.createdAt, _whole(smuggled), _whole(data.received.revealed[0].value) + ); + bytes memory sig = _signWith(TEST_KEY, forged); + + vm.expectRevert(abi.encodeWithSelector(CeremonyAttestation.BareLineFeed.selector, REQUEST_LINE.length + 6)); + roots.rotate{value: FEE}(forged, sig); + } + + // ─── Helpers ──────────────────────────────────────────────────── + + /// `CeremonyAttestation.decode` reads calldata, so the record goes out + /// through an external call to come back decoded. + function decode(bytes calldata data) external pure returns (CeremonyAttestation.AttestedData memory) { + return CeremonyAttestation.decode(data); + } + + /// Google's key set as the contract reads it: the received direction past + /// the head, de-chunked. + function _realBody() private view returns (bytes memory) { + return _dechunk(_afterHead(this.decode(record).received.revealed[0].value)); + } + + function _signWith(uint256 key, bytes memory attested) private pure returns (bytes memory) { + bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(keccak256(attested)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, ethHash); + return abi.encodePacked(r, s, v); + } + + /// The JWKS layout: one revealed range covering the whole direction, + /// nothing committed. + function _whole(bytes memory transcript) private pure returns (AttestationBuilder.Direction memory) { + return AttestationBuilder.Direction({ + revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: transcript})), + commitments: AttestationBuilder.none(), + length: uint32(transcript.length) + }); + } + + function _head(bytes memory message) private pure returns (bytes memory) { + uint256 boundary = _indexOf(message, HEAD_BOUNDARY, 0); + assertTrue(boundary != NONE, "no head boundary"); + return _slice(message, 0, boundary + 2); + } + + function _afterHead(bytes memory message) private pure returns (bytes memory) { + uint256 boundary = _indexOf(message, HEAD_BOUNDARY, 0); + assertTrue(boundary != NONE, "no head boundary"); + return _slice(message, boundary + 4, message.length); + } + + /// A strict test-side de-chunker: hex size, CRLF, data, CRLF; a zero + /// size followed by CRLF and nothing more ends it. + function _dechunk(bytes memory chunked) private pure returns (bytes memory out) { + uint256 at; + while (true) { + uint256 size; + uint256 lineEnd = _indexOf(chunked, "\r\n", at); + assertTrue(lineEnd != NONE && lineEnd > at, "chunk size line"); + for (; at < lineEnd; ++at) { + uint8 c = uint8(chunked[at]); + uint256 nibble = c <= 0x39 ? c - 0x30 : (c | 0x20) - 0x61 + 10; + size = (size << 4) | nibble; + } + at += 2; + if (size == 0) { + assertEq(chunked.length - at, 2, "no trailers"); + return out; + } + out = abi.encodePacked(out, _slice(chunked, at, at + size)); + at += size; + assertEq(_slice(chunked, at, at + 2), bytes("\r\n"), "CRLF after chunk data"); + at += 2; + } + } + + function _count(bytes memory haystack, bytes memory needle) private pure returns (uint256 count) { + for (uint256 at = _indexOf(haystack, needle, 0); at != NONE; at = _indexOf(haystack, needle, at + 1)) { + ++count; + } + } + + function _contains(bytes memory haystack, bytes memory needle) private pure returns (bool) { + return _indexOf(haystack, needle, 0) != NONE; + } + + function _indexOf(bytes memory haystack, bytes memory needle, uint256 from) private pure returns (uint256) { + for (uint256 i = from; i + needle.length <= haystack.length; ++i) { + bool hit = true; + for (uint256 j = 0; j < needle.length; ++j) { + if (haystack[i + j] != needle[j]) { + hit = false; + break; + } + } + if (hit) return i; + } + return NONE; + } + + function _startsWith(bytes memory data, bytes memory prefix) private pure returns (bool) { + return data.length >= prefix.length && keccak256(_slice(data, 0, prefix.length)) == keccak256(prefix); + } + + function _endsWith(bytes memory data, bytes memory suffix) private pure returns (bool) { + return data.length >= suffix.length + && keccak256(_slice(data, data.length - suffix.length, data.length)) == keccak256(suffix); + } + + function _slice(bytes memory data, uint256 from, uint256 to) private pure returns (bytes memory out) { + out = new bytes(to - from); + for (uint256 i = 0; i < out.length; ++i) { + out[i] = data[from + i]; + } + } +} diff --git a/solidity/contracts/ceremony/test/fixtures/google-jwks-session.json b/solidity/contracts/ceremony/test/fixtures/google-jwks-session.json new file mode 100644 index 00000000..c1a15756 --- /dev/null +++ b/solidity/contracts/ceremony/test/fixtures/google-jwks-session.json @@ -0,0 +1,8 @@ +{ + "notary": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "created_at": 1788431527, + "attested_data": "0x59962e64d51a4d36e444d18d029aff438ce3ddb882eb151a0af0fdde3cb655ee000000006a994ca70000008800000a0b0000000000000001000000000000000000000088474554202f6f61757468322f76332f636572747320485454502f312e310d0a686f73743a207777772e676f6f676c65617069732e636f6d0d0a636f6e6e656374696f6e3a20636c6f73650d0a6163636570743a206170706c69636174696f6e2f6a736f6e0d0a757365722d6167656e743a206c696269642d6b65657065722f302e332e300d0a0d0a00000000000000000000000000000001000000000000000000000a0b485454502f312e3120323030204f4b0d0a5365727665723a2073636166666f6c64696e67206f6e2048545450536572766572320d0a582d5853532d50726f74656374696f6e3a20300d0a582d4672616d652d4f7074696f6e733a2053414d454f524947494e0d0a582d436f6e74656e742d547970652d4f7074696f6e733a206e6f736e6966660d0a446174653a205468752c2030332053657020323032362031303a33313a333520474d540d0a457870697265733a205468752c2030332053657020323032362031353a35343a333120474d540d0a43616368652d436f6e74726f6c3a207075626c69632c206d61782d6167653d31393337362c206d7573742d726576616c69646174652c206e6f2d7472616e73666f726d0d0a436f6e74656e742d547970653a206170706c69636174696f6e2f6a736f6e3b20636861727365743d5554462d380d0a4167653a2033320d0a416c742d5376633a2068333d223a343433223b206d613d323539323030302c68332d32393d223a343433223b206d613d323539323030300d0a4163636570742d52616e6765733a206e6f6e650d0a566172793a204f726967696e2c582d4f726967696e2c526566657265722c4163636570742d456e636f64696e670d0a436f6e6e656374696f6e3a20636c6f73650d0a5472616e736665722d456e636f64696e673a206368756e6b65640d0a0d0a3766660d0a7b0a2020226b657973223a205b0a202020207b0a20202020202022757365223a2022736967222c0a2020202020202265223a202241514142222c0a202020202020226b6964223a202264366130653635396333333932643965353932613462306164383865376232623838303736393039222c0a20202020202022616c67223a20225253323536222c0a202020202020226e223a2022784933496e584f326b4671376a647733484b4c43584d754a77455462496b6b4930516a634249314858444563444e4a496a694345564d3063383633385f43674979586e5f55577459726f6764744766707046354b684e6e3935716b4a306f55375f50696d4432474f7833672d766c6a354c4f325f4c6f6f4846386c65626368674e71735f71536f6a47516a4e7653504d5a42543137373037676f792d6630646f4c6d55556a726b675a30675746776550625f353343704841736c6f5f4a666b507050737461592d4b69646b5a6c36784247446f4f42796f58345a655f736141446d51525061303770476e74636c765452526f75753575627256644d6561414b746356367779494844694f593354757a6573397459674a4644546e35345a6d39684d45414e7668514c645077616377764d5f5970456834475270376a693030513139735f4f514332557a4275334747346e72346a6d7377222c0a202020202020226b7479223a2022525341220a202020207d2c0a202020207b0a2020202020202265223a202241514142222c0a202020202020226b7479223a2022525341222c0a20202020202022616c67223a20225253323536222c0a202020202020226e223a202271686a3546576955444c66756541364f564c626c66585a39414a74374e4c6a464b69546b3875737052616a6b597a545f5269516c426d687652616345715843415931504735796c504a6161656978345965756651486f5a6e676a486d453276376b364f764a6e55414a45384e65347a7a7641386d4551704e627247734634423067334b73303177665762577857516a5a4630684330736173324470484e436a476a75493176537763717164416e70587a4d3442384443714b68394b4461435f4a52325f432d535457685168733352705a4a6f4c695931566a5856704d757937574b426537517253365855546542585f4c4470396137613353366d764a33587555327a4c66715176647869367a3748654f4b75374356667842586a6d574237716b4334422d494e777436676f416841444678733066775f4a41747347307864734c5630446c544c6c635567686238416f63734453725751222c0a202020202020226b6964223a202261386638306235313234363939353963646331656562613434623036366332663739393434373739222c0a20202020202022757365223a2022736967220a202020207d2c0a202020207b0a202020202020226b7479223a2022525341222c0a20202020202022616c67223a20225253323536222c0a202020202020226e223a20227049706e7a4132657a794545524a53786971704c426d4d6549714154482d5636697542744b496962584579596f76756a7278386e697154654f3652497958543675445555763056326b4a38565f69575946787a58593142714b39496663416d6a67305855446f7954566b6f794c734630676a3239394c482d7a773576437679386a6d616d46495a4b41624b63513568704876536974744d31766c2d3676564c2d693247787947624d4139615936487131354e796c53317437454c54596651696d6c6e76786362375f444d30637553355531536662435a4d43704b6868306e72536c5964733234306f7870434a4f563272426168735f456135633774657a53316e77564339575f452d62523954463642486b435f66762d4538446357666b495f366765614a7a4268494e4e7842666a782d77312d575570324a7a3359594657456665516a784d71752d466736634778776b375631367551222c0a202020202020226b6964223a202239343361336135643764393139363235613435346534383962373563323961646162353761636261222c0a2020202020202265223a202241514142222c0a20202020202022757365223a2022736967220a202020207d2c0a202020207b0a20202020202022616c67223a20225253323536222c0a2020202020202265223a202241514142222c0a202020202020226b7479223a2022525341222c0a20202020202022757365223a2022736967222c0a202020202020226b6964223a202266313066383734303561393739633164663336646632363630363733346633336364383563323731222c0a202020202020226e223a20223472593575775a4b3164512d555667423573344e4c79432d75354c43324d5437623847575a7a74694e674d7370304e6e717830704d374f667830777333324e32615a637831302d4a38796451786e4e6239754163662d374c7968794f4963765f5745797a61536255414d4f676f462d6e516d4a6574636b784e6736656b684e6661466354515330542d3239716c325f43424c494d4c36437653682d72306667575273714e326179423777436c37344776364f4f56627661675557686a357a324c366f5f706c6d735044774c56757641376f33574445446a6f712d4958616652516f776a39326b5155656e724f4b443459436f70754c494268656c36564838646f46524e5a364b495351684d634f697657614c555f55744b4b414d6c6f474a696554665f33722d5f6e457273326835774237543746724d4353636d4f376d764651584b68385f34502d4d6c6266675339435576516b7377220a202020207d0a20205d0a7d0a0d0a300d0a0d0a0000000000000000", + "notary_signature": "0xfa3c338f8e2031d3daa408c98a29981a49f5af87bb394f32d660eadedeb85b3f43657694e2128f7e9b9acd268039f16f05d10c4da21528ca900406b64b5ff0351b", + "captured_at": "2026-09-03T10:32:07Z", + "endpoint": "https://www.googleapis.com/oauth2/v3/certs" +} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 33ba7640..1cc6fa50 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -20,7 +20,9 @@ cbor_metadata = false fs_permissions = [ # Google's real JWKS body, as served today, so the root list's parser is # tested against the document it will actually read rather than a shape - # a test author remembered (see GoogleJwtRoots.t.sol). + # a test author remembered (see GoogleJwtRoots.t.sol) -- and a whole + # notarized session of it, signature included, so the real path is tested + # end to end (see GoogleJwtRootsRealSession.t.sol). { access = "read", path = "contracts/ceremony/test/fixtures" }, { access = "read", path = "out" }, # the factory tests read creation code from the artifacts ] From 02d098604d4955de234d1cf81bbb5680bea1daf3 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 12:34:38 +0100 Subject: [PATCH 08/15] refactor(ceremony)!: GoogleJwtRoots keeps two generations of Google's key set, and no history The list was a per-kid state machine: a modulus, an expiry and a monotonic stamp per kid, a tracked-kid enumeration with a cap, a prune anyone could call, an untrust only the owner could. Google's JWKS is none of that. It is an unordered set of two to four keys with no "next" marker, so the faithful form of a reading is the set itself -- and the decision is: no history of keys, no maintenance, bounded state. "We don't need to store the history of all keys, and we don't need to require any maintenance. No one will call untrustModulus." So the list is two generations of the set and nothing else. `current` is the latest reading applied (Google's set already lists the key it will sign with next), `previous` the reading before it. A newer reading of the same set only restarts `current`'s lifetime; a newer reading of a different set shifts `current` into `previous` and drops what `previous` held. `previous` is kept because Google's rotation and the keeper's reading are not simultaneous: a token minted under the key Google just rotated out is still being presented for the rest of its hour, and a keeper's reading can land in between. A generation is trusted until READING_LIFETIME after its reading's own `createdAt` -- the notary's clock -- so trust lapses by time alone, with no prune, no untrust, no transaction from anyone. A reading is read whole before any of it is written (`EmptyKeySet`, `TooManyKeys(count)`, `DuplicateKey(modulusHash)`), a reading dated no later than the one in force is ignored rather than refused (a front-runner must not brick the keeper), and the set comparison is order-insensitive. Removed: modulusOfKid, expiresAtKid, rotatedAtKid, the tracked-kid set, MAX_TRACKED_KIDS, DEFAULT_MODULUS_TTL, prune, untrustModulus, currentRoots, RootInfo, the four per-key events and TooManyKids. Added: MAX_KEYS, READING_LIFETIME, currentKeys(), and the events KeysRotated(observedAt, kids, moduli) / ReadingRefreshed. `trustedHashExpiresAt`, what GooglePlatformVerifier reads, is unchanged in shape. The ERC-7201 slot is unchanged; the struct behind it is not. Three refusals from the audit ride along, since they are checks on the same reading: a query in the request target (`?callback=` turns the same path into JSONP whose body begins with requester-chosen bytes) is refused by the pinned request line; a `Content-Encoding` on the response is refused by name (`EncodedBody`) rather than as some byte deep in the JSON walk; and every JWK's `e` must be exactly `AQAB` (`WrongExponent`) -- the google/v1 circuit verifies RS256 under 65537 and the verifier trusts a key by its modulus alone. Rust and TS follow the ABI: the binding declares `Generation`, `currentKeys`, `KeysRotated`/`ReadingRefreshed`; the anvil test reads both generations empty; the READMEs point at `currentKeys`. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/src/bindings/ceremony.rs | 51 +- rust/contracts/tests/anvil.rs | 9 +- .../contracts/ceremony/GoogleJwtRoots.sol | 442 +++++++------- .../ceremony/test/GoogleJwtRoots.t.sol | 563 +++++++++++------- .../test/GoogleJwtRootsRealSession.t.sol | 58 +- .../ceremony/test/UpgradeSafety.t.sol | 68 ++- ts/packages/contracts/README.md | 5 +- 7 files changed, 686 insertions(+), 510 deletions(-) diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index 3175a198..1f07e5c9 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -96,6 +96,15 @@ pub use proof_verifier_inner::CeremonyProofVerifier; /// Google names bind only once a notarized reading of Google's JWKS has /// landed here. /// +/// The list is two generations of Google's key set and nothing else: +/// `current` is the latest reading applied, `previous` the reading before +/// it, kept for the tokens still in flight under a key Google has since +/// dropped. A newer reading of the same set restarts `current`'s clock +/// (`ReadingRefreshed`); a newer reading of a different set shifts `current` +/// into `previous` and drops what `previous` held (`KeysRotated`). A +/// generation is trusted until `READING_LIFETIME` after its reading's own +/// `createdAt`, so there is nothing to prune or untrust by hand. +/// /// A rotation is an ordinary notarized session: the keeper reveals the whole /// `GET /oauth2/v3/certs` exchange, `rotate` hands the attested bytes and the /// notary's proof to the Notary Service with the Notary Fee attached (read @@ -108,12 +117,12 @@ mod google_jwt_roots_inner { sol! { #[sol(rpc)] interface GoogleJwtRoots { + /// One reading of Google's key set: the notary's clock, and the + /// limb hash of every modulus it listed, in Google's order. #[derive(Debug, serde::Serialize, serde::Deserialize)] - struct RootInfo { - bytes32 kidHash; - bytes32 modulusHash; - uint256 observedAt; - uint256 expiresAt; + struct Generation { + uint64 observedAt; + bytes32[] moduli; } function initialize(address owner_, address notary_) external; @@ -125,20 +134,25 @@ mod google_jwt_roots_inner { function quoteRotation() external view returns (uint256); /// Permissionless. `attestedData` is the ceremony-common section /// 9.1 record of the JWKS session, `proof` the notary's - /// authentication of it (a 65-byte EIP-191 signature today). + /// authentication of it (a 65-byte EIP-191 signature today). A + /// reading dated no later than the current generation is + /// ignored, not refused. function rotate(bytes calldata attestedData, bytes calldata proof) external payable; - function untrustModulus(bytes32 modulusHash) external; - function prune() external; - function modulusOfKid(bytes32 kidHash) external view returns (bytes32); - function expiresAtKid(bytes32 kidHash) external view returns (uint256); - /// What `GooglePlatformVerifier` reads: modulus hash -> expiry, - /// zero when untrusted. + /// What `GooglePlatformVerifier` reads: modulus hash -> when it + /// stops being trusted, zero when neither generation lists it. function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256); - function rotatedAtKid(bytes32 kidHash) external view returns (uint256); + /// Both generations, as stored. + function currentKeys() external view returns (Generation memory current, Generation memory previous); + /// The current generation's `observedAt`: the notary's clock on + /// the reading in force. function freshestObservedAt() external view returns (uint256); - function currentRoots() external view returns (RootInfo[] memory); + /// True until the current generation is guaranteed trusted + /// `RENEWAL_MARGIN` from now; true on an empty list. function needsRotation() external view returns (bool); + function READING_LIFETIME() external view returns (uint256); + function RENEWAL_MARGIN() external view returns (uint256); + function MAX_KEYS() external view returns (uint256); function owner() external view returns (address); function pendingOwner() external view returns (address); @@ -146,10 +160,11 @@ mod google_jwt_roots_inner { function acceptOwnership() external; event NotaryServiceChanged(address notary); - event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); - event ModulusUntrusted(bytes32 indexed modulusHash); - event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); + /// A different set landed: `kids` and `moduli` in the order + /// Google listed them, `observedAt` the notary's clock. + event KeysRotated(uint64 observedAt, string[] kids, bytes32[] moduli); + /// A newer reading of the set already current. + event ReadingRefreshed(uint64 observedAt); } } } diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index 039c88c9..244e4af8 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -154,12 +154,17 @@ async fn deploys_the_identity_stack_behind_proxies() { ); // The root list points at the Notary Service, quotes its fee, and starts - // empty — so it wants a rotation before any Google name can bind. + // with both generations empty — so it wants a rotation before any Google + // name can bind. let roots = GoogleJwtRoots::new(roots_proxy, &provider); assert_eq!(roots.notaryService().call().await.unwrap(), notary_proxy); assert_eq!(roots.quoteRotation().call().await.unwrap(), fee); assert!(roots.needsRotation().call().await.unwrap()); - assert!(roots.currentRoots().call().await.unwrap().is_empty()); + let keys = roots.currentKeys().call().await.unwrap(); + assert_eq!(keys.current.observedAt, 0); + assert!(keys.current.moduli.is_empty()); + assert_eq!(keys.previous.observedAt, 0); + assert!(keys.previous.moduli.is_empty()); } /// (b) The Notary Service lifecycle: deploy behind a proxy, add a second diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index 29ad90f0..f4584b87 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -16,7 +16,27 @@ import {INotaryService} from "./INotaryService.sol"; /// notarized reading of Google's published JWKS: a JWT verifies only /// while the modulus that signed it is listed here and unexpired. /// -/// @dev The reading is an ordinary notarized session -- the section 9.1 +/// @dev The list is two generations of Google's key set, and nothing else. +/// Google's JWKS is an unordered set of a few keys with no "next" +/// marker, so a reading is stored as the set it is: `current` is the +/// latest reading applied, `previous` the reading before it. A newer +/// reading of the same set only restarts `current`'s lifetime; a newer +/// reading of a different set replaces -- `current` becomes `previous`, +/// the reading becomes `current`, and what `previous` held is gone. +/// There is no history and no maintenance: nothing to prune, nothing to +/// untrust, no owner lever over which of Google's keys are trusted. +/// +/// A generation is trusted until READING_LIFETIME after its reading's +/// own `createdAt` -- the notary's clock, not the block the rotation +/// landed in -- so trust lapses by time alone, with no transaction from +/// anyone, and a later reading of the same set extends it. `previous` +/// is kept because Google's rotation and the keeper's reading are not +/// simultaneous: a token minted under the key Google just rotated out +/// is still being presented for the rest of its hour, and a keeper's +/// reading can land in between. One generation back covers that; +/// further back is history. +/// +/// The reading is an ordinary notarized session -- the section 9.1 /// record every Platform Verifier consumes, authenticated by the same /// Notary Service and charged the same Notary Fee. What differs is the /// layout the keeper's prover produces: everything in both directions is @@ -29,9 +49,9 @@ import {INotaryService} from "./INotaryService.sol"; /// an open caller set a one-shot nullifier would BE the attack: a /// front-runner could consume a reading's digest and brick the honest /// keeper. Freshness comes from the notary's signed `createdAt` plus the -/// window below, and re-applying the same reading is idempotent. A -/// rotation pays the Notary Fee, which is the one thing a submitter -/// needs beyond gas. +/// window below, and a reading dated no later than the one in force is +/// ignored rather than refused. A rotation pays the Notary Fee, which is +/// the one thing a submitter needs beyond gas. /// /// **The reading names no contract.** The attested record is the TLS /// session alone -- authority, notary clock, transcript -- so the same @@ -46,39 +66,31 @@ import {INotaryService} from "./INotaryService.sol"; /// and a second OIDC provider would want the same list. /// /// **The trust model, in one line: the notary attests the reading, time -/// does the expiry, and nobody owns rotation.** Every entry carries a -/// stamp, verifiers check the stamp at use-site, and an expired key stops -/// being trusted with no transaction from anyone. The owner's only lever -/// over the key list is `untrustModulus`, which can only REMOVE trust. +/// does the expiry, and nobody owns rotation.** Verifiers check the stamp +/// at use-site, and an expired generation stops being trusted with no +/// transaction from anyone. The owner's one lever is the Notary Service +/// pointer: which notary a reading must carry, never which Google key +/// is trusted. contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable { // ─── State ────────────────────────────────────────────────────── + /// One reading of Google's key set: when the notary read it, and the + /// limb hash of every modulus it listed, in the order Google listed them. + struct Generation { + uint64 observedAt; + bytes32[] moduli; + } + /// @custom:storage-location erc7201:libid.storage.GoogleJwtRoots struct GoogleJwtRootsStorage { /// Authenticates a reading and charges for it. INotaryService notary; - /// kid keccak -> the limb-keccak the circuit produces. - mapping(bytes32 => bytes32) modulusOfKid; - mapping(bytes32 => uint256) expiresAtKid; - /// modulusHash -> expiry. This is what a verifier reads: the JWT - /// circuit does not expose `kid`, so trust has to be checkable from - /// the modulus alone. - mapping(bytes32 => uint256) trustedHashExpiresAt; - /// When each kid was last written, by the reading's own `createdAt`. - /// Rotation is open, so without this a replayed older reading inside - /// the freshness window would roll a kid back to a modulus Google has - /// retired and re-stamp it for another TTL. - mapping(bytes32 => uint256) rotatedAtKid; - /// Every kid currently tracked, so keepers can enumerate the list - /// without an indexer. Bounded by MAX_TRACKED_KIDS; expired entries - /// leave via `prune()`, which anyone may call. - bytes32[] trackedKids; - /// kid keccak -> index+1 in `trackedKids` (0 = not tracked). - mapping(bytes32 => uint256) trackedKidIndex; - /// The timestamp of the freshest reading ever applied -- the notary's - /// observation time, not the block it landed in. A keeper compares - /// this to wall-clock to decide whether its reading would be news. - uint256 freshestObservedAt; + /// The latest reading applied. Google's set already lists the key it + /// will sign with next, so this is Google's current AND next keys. + Generation current; + /// The reading before it, kept for the tokens still in flight under a + /// key Google has since dropped. + Generation previous; } // keccak256(abi.encode(uint256(keccak256("libid.storage.GoogleJwtRoots")) - 1)) & ~bytes32(uint256(0xff)) @@ -97,20 +109,25 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab bytes32 public constant AUTHORITY = keccak256("www.googleapis.com"); uint256 public constant FRESHNESS_WINDOW = 1 hours; uint256 public constant CLOCK_SKEW_GRACE = 5 minutes; - uint256 public constant DEFAULT_MODULUS_TTL = 30 days; - /// The most distinct kids the list will track at once. Google publishes a - /// handful of overlapping keys; a notarized reading cannot invent kids, so - /// this cap is headroom, not a working limit. It exists so the enumeration - /// below stays a bounded loop no submitter -- honest or not -- can bloat. - uint256 public constant MAX_TRACKED_KIDS = 16; - /// How much trusted runway `needsRotation()` insists on. Rotation re-stamps - /// every key for DEFAULT_MODULUS_TTL, so a keeper acting on this signal + /// How long a generation is trusted after the reading that wrote it, by + /// the reading's own clock. Google's keys overlap for days and a keeper + /// reads weekly, so this is runway for a keeper outage, not a key's life. + uint256 public constant READING_LIFETIME = 30 days; + /// The most keys one reading may carry. Google served four on 2026-09-03, + /// and a notarized reading cannot invent any, so this is headroom: it + /// bounds the set comparison below, not what Google publishes. + uint256 public constant MAX_KEYS = 8; + /// How much trusted runway `needsRotation()` insists on. A reading stamps + /// its set for READING_LIFETIME, so a keeper acting on this signal /// rotates roughly weekly rather than racing the expiry. uint256 public constant RENEWAL_MARGIN = 7 days; /// @dev The request line the keeper's prover puts on the wire, in /// origin-form like every other verifier pins it. The path is what - /// separates the key set from everything else googleapis.com serves. + /// separates the key set from everything else googleapis.com serves, + /// and the whole line -- through the CRLF -- is what keeps a query + /// out: `?callback=` turns the same path into JSONP, a 200 whose + /// body begins with bytes the requester chose. bytes private constant REQUEST_LINE = "GET /oauth2/v3/certs HTTP/1.1\r\n"; /// @dev googleapis.com serves many virtual hosts under one certificate, so /// the TLS authority alone does not pin which backend answered. The @@ -122,7 +139,11 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab bytes private constant TRANSFER_ENCODING_NEEDLE = "\r\ntransfer-encoding:"; bytes private constant CHUNKED_NEEDLE = "\r\ntransfer-encoding:chunked\r\n"; bytes private constant CONTENT_LENGTH_NEEDLE = "\r\ncontent-length:"; + bytes private constant CONTENT_ENCODING_NEEDLE = "\r\ncontent-encoding:"; bytes private constant KEYS_TOKEN = '"keys"'; + /// @dev 65537, the one public exponent the `google/v1` circuit verifies + /// RS256 under. + bytes private constant EXPECTED_EXPONENT = "AQAB"; uint256 private constant MODULUS_BYTES = 256; uint256 private constant MODULUS_LIMBS = 18; @@ -130,24 +151,33 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab // ─── Events ───────────────────────────────────────────────────── event NotaryServiceChanged(address notary); - event ModulusRotated(bytes32 indexed kidHash, string kid, bytes32 modulusHash, uint256 expiresAt); - event ModulusUntrusted(bytes32 indexed modulusHash); - /// One key written by a rotation, with the reading's own timestamp. What a - /// keeper watches: `observedAt` orders readings, `expiresAt` says when this - /// entry stops being trusted if no rotation lands before then. - event RootApplied(bytes32 indexed kidHash, bytes32 indexed modulusHash, uint256 observedAt, uint256 expiresAt); - /// An expired key left the tracked set. Permissionless -- time retired the - /// key, `prune()` merely reclaimed the slot. - event RootPruned(bytes32 indexed kidHash, bytes32 modulusHash); + /// A different set than `current` landed: `current` became `previous`, + /// this reading became `current`. `kids` and `moduli` are the reading's + /// keys in the order Google listed them, `observedAt` the notary's clock + /// -- enough for a keeper to follow the list from logs alone. + event KeysRotated(uint64 observedAt, string[] kids, bytes32[] moduli); + /// A newer reading of the set already current: nothing shifted, and the + /// set's lifetime restarted from this reading's clock. + event ReadingRefreshed(uint64 observedAt); // ─── Errors ───────────────────────────────────────────────────── error ZeroAddress(); - error TooManyKids(); /// @dev A reading whose key set is empty says nothing about what Google - /// publishes; applying it would move `freshestObservedAt` while - /// writing no key and emitting nothing. Refused rather than charged. + /// publishes; applying it would shift the live set into `previous` + /// on the strength of a document that names no key. Refused rather + /// than charged. error EmptyKeySet(); + /// @dev More keys in one reading than MAX_KEYS. + error TooManyKeys(uint256 count); + /// @dev The same modulus twice in one reading. Google lists each key + /// once; a document that repeats one is not the document this + /// reads, and a set with a repeat would compare as a set it is not. + error DuplicateKey(bytes32 modulusHash); + /// @dev A JWK whose public exponent is not 65537. The verifier trusts a + /// key by its modulus alone, so a key that would verify under a + /// different exponent must never be listed. + error WrongExponent(bytes found); /// @dev Exact value, both ways: the Notary Fee is forwarded whole, so there /// is no overpayment to refund and nothing captured in transit. error WrongValue(uint256 required, uint256 provided); @@ -185,8 +215,12 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// @dev Both `Transfer-Encoding` and `Content-Length` are present, so two /// parsers could frame two bodies. error ConflictingFraming(); + /// @dev The response declares a `Content-Encoding`. The prover must not + /// negotiate compression: gzip bytes would only fail later, somewhere + /// in the JSON walk, and an explicit refusal names the cause. + error EncodedBody(); /// @dev The member is absent, or present but not in the shape read here: - /// `keys` must be an array, `kid` and `n` must be strings. + /// `keys` must be an array, `kid`, `n` and `e` must be strings. error MissingMember(string name); error AmbiguousMember(string name); /// @dev A string value with no closing quote, or the `keys` array with no @@ -269,112 +303,54 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab // told: the TLS server name it authenticated in the handshake. if (data.authorityId != AUTHORITY) revert WrongAuthority(AUTHORITY, data.authorityId); - uint256 createdAt = data.createdAt; + uint64 createdAt = data.createdAt; if (createdAt > block.timestamp + CLOCK_SKEW_GRACE) revert FutureProof(); if (block.timestamp > createdAt + FRESHNESS_WINDOW) revert StaleProof(); _requireJwksRequest(data.sent, data.sentTranscriptLength); bytes memory body = _jwksBody(data.received, data.recvTranscriptLength); - // Monotonic high-water mark of reading timestamps. Replaying an older - // reading leaves it alone, so it only ever moves forward. - if (createdAt > $.freshestObservedAt) $.freshestObservedAt = createdAt; - - uint256 expiry = block.timestamp + DEFAULT_MODULUS_TTL; - _applyKeys(body, expiry, createdAt); + // The whole set is read before any of it is written: a reading is + // applied entire or refused entire, never half. + (string[] memory kids, bytes32[] memory moduli) = _readKeys(body); + _apply(createdAt, kids, moduli); } - /// @notice Stop trusting a key before anything replaces it. - /// - /// @dev Rotation already retires the key a kid used to carry, which covers - /// the ordinary case. This covers the one that cannot wait: a key - /// Google has not rotated yet, or has rotated in a way this list has - /// not seen. Without it the only remedy for a compromised modulus is - /// an upgrade, and a thirty-day stamp is a long time to hold a key - /// nobody should be signing with. - /// - /// Owner-only, and it can only ever REMOVE trust: the worst an owner - /// does with it is refuse bindings, which the rotation any keeper can - /// submit undoes. - function untrustModulus(bytes32 modulusHash) external onlyOwner { - delete _s().trustedHashExpiresAt[modulusHash]; - emit ModulusUntrusted(modulusHash); - } - - /// @notice Drop every expired kid from the tracked set. Anyone may call it. - /// - /// @dev Expiry itself needs no transaction -- verifiers check the stamp at - /// use-site, so an expired key is already untrusted the second its TTL - /// passes. Pruning only reclaims enumeration slots (and the storage - /// refund). Rotation calls this by itself when the set is full, so - /// even the prune is optional housekeeping. - function prune() external { - _pruneExpired(); - } - - // ─── Keeper views ─────────────────────────────────────────────── + // ─── What a verifier, and a keeper, read ──────────────────────── - /// One tracked key, as a keeper sees it. - struct RootInfo { - bytes32 kidHash; - bytes32 modulusHash; - /// The notarized reading that last wrote this kid (its `createdAt`). - uint256 observedAt; - /// When this entry stops being trusted, absent a fresher rotation. - uint256 expiresAt; - } - - function modulusOfKid(bytes32 kidHash) external view returns (bytes32) { - return _s().modulusOfKid[kidHash]; - } - - function expiresAtKid(bytes32 kidHash) external view returns (uint256) { - return _s().expiresAtKid[kidHash]; - } - - /// @notice modulusHash -> expiry. What `GooglePlatformVerifier` reads. + /// @notice modulusHash -> when it stops being trusted, or zero. What + /// `GooglePlatformVerifier` reads: the JWT circuit does not expose + /// `kid`, so trust has to be checkable from the modulus alone. + /// @dev A key in both generations reports the current one's stamp: the + /// later reading is the later word that Google still lists it. function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256) { - return _s().trustedHashExpiresAt[modulusHash]; + GoogleJwtRootsStorage storage $ = _s(); + if (_lists($.current.moduli, modulusHash)) return uint256($.current.observedAt) + READING_LIFETIME; + if (_lists($.previous.moduli, modulusHash)) return uint256($.previous.observedAt) + READING_LIFETIME; + return 0; } - function rotatedAtKid(bytes32 kidHash) external view returns (uint256) { - return _s().rotatedAtKid[kidHash]; + /// @notice Both generations, as stored. One call tells a keeper the whole + /// state of the list; an empty `current` is a list nothing has + /// been read into yet. + function currentKeys() external view returns (Generation memory current, Generation memory previous) { + GoogleJwtRootsStorage storage $ = _s(); + return ($.current, $.previous); } + /// @notice The `createdAt` of the reading in force -- the notary's clock, + /// not the block it landed in. A keeper compares this to + /// wall-clock to decide whether its reading would be news. function freshestObservedAt() external view returns (uint256) { - return _s().freshestObservedAt; - } - - /// @notice Every tracked key with its provenance and expiry, expired ones - /// included until someone prunes. One call tells a keeper the whole - /// state of the list. - function currentRoots() external view returns (RootInfo[] memory infos) { - GoogleJwtRootsStorage storage $ = _s(); - uint256 n = $.trackedKids.length; - infos = new RootInfo[](n); - for (uint256 i = 0; i < n; ++i) { - bytes32 kidHash = $.trackedKids[i]; - infos[i] = RootInfo({ - kidHash: kidHash, - modulusHash: $.modulusOfKid[kidHash], - observedAt: $.rotatedAtKid[kidHash], - expiresAt: $.expiresAtKid[kidHash] - }); - } + return _s().current.observedAt; } - /// @notice True when no key is guaranteed trusted RENEWAL_MARGIN from now -- - /// the single bit a keeper polls to decide whether to fetch a fresh - /// reading. True from deployment until the first rotation lands. + /// @notice True when the current generation is not guaranteed trusted + /// RENEWAL_MARGIN from now -- the single bit a keeper polls to + /// decide whether to fetch a fresh reading. True from deployment + /// until the first rotation lands. function needsRotation() external view returns (bool) { - GoogleJwtRootsStorage storage $ = _s(); - uint256 horizon = block.timestamp + RENEWAL_MARGIN; - uint256 n = $.trackedKids.length; - for (uint256 i = 0; i < n; ++i) { - bytes32 modulusHash = $.modulusOfKid[$.trackedKids[i]]; - if ($.trustedHashExpiresAt[modulusHash] > horizon) return false; - } - return true; + return block.timestamp + RENEWAL_MARGIN >= uint256(_s().current.observedAt) + READING_LIFETIME; } // ─── The transcript ───────────────────────────────────────────── @@ -467,6 +443,12 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab body = _slice(received, boundary + 4, received.length); bytes memory normalized = CeremonyAttestation.normalizeHeaderBytes(head); + // The body must be the document, not a compressed form of it. The + // prover sends no `Accept-Encoding`; a server that compresses anyway + // is refused by name rather than somewhere in the JSON walk. + (uint256 encodings,) = _find(normalized, CONTENT_ENCODING_NEEDLE); + if (encodings != 0) revert EncodedBody(); + (uint256 codings,) = _find(normalized, TRANSFER_ENCODING_NEEDLE); (uint256 lengths, uint256 at) = _find(normalized, CONTENT_LENGTH_NEEDLE); // Exactly one framing header, of one kind. Two of either, or one of @@ -550,17 +532,18 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab // ─── The key set ──────────────────────────────────────────────── - /// @dev Read every JWK out of the body and apply it. The body is Google's - /// pretty-printed JSON; this reads exactly the shape Google publishes - /// and refuses anything it does not recognise, so it is a checker - /// for one document rather than a JSON parser. + /// @dev Read every JWK out of the body, writing nothing. The body is + /// Google's pretty-printed JSON; this reads exactly the shape Google + /// publishes and refuses anything it does not recognise, so it is a + /// checker for one document rather than a JSON parser. /// /// `keys` is located by counting: the token appears once in the /// whole body, or the document is not the one this reads. Objects are /// flat -- a `{` or `[` inside one is refused -- so each ends at its - /// first `}` and the walk is one forward pass bounded by the body, - /// with the object count capped at MAX_TRACKED_KIDS. - function _applyKeys(bytes memory body, uint256 expiry, uint256 provenAt) private { + /// first `}` and the walk is one forward pass bounded by the body. + /// Past MAX_KEYS the walk only counts, so the refusal names how many + /// keys the reading carries rather than the first one too many. + function _readKeys(bytes memory body) private pure returns (string[] memory kids, bytes32[] memory moduli) { (uint256 count, uint256 at) = _find(body, KEYS_TOKEN); if (count == 0) revert MissingMember("keys"); if (count > 1) revert AmbiguousMember("keys"); @@ -571,6 +554,8 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab if (at >= body.length || body[at] != "[") revert MissingMember("keys"); ++at; + kids = new string[](MAX_KEYS); + moduli = new bytes32[](MAX_KEYS); uint256 keys; while (true) { at = _ws(body, at); @@ -585,19 +570,32 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab if (at >= body.length) revert UnterminatedMember("keys"); } if (body[at] != "{") revert UnterminatedMember("keys"); - if (keys == MAX_TRACKED_KIDS) revert TooManyKids(); uint256 close = _objectEnd(body, at); - _applyKey(_slice(body, at, close + 1), expiry, provenAt); + if (keys < MAX_KEYS) (kids[keys], moduli[keys]) = _readKey(_slice(body, at, close + 1)); ++keys; at = close + 1; } if (keys == 0) revert EmptyKeySet(); + if (keys > MAX_KEYS) revert TooManyKeys(keys); at = _ws(body, at); if (at >= body.length || body[at] != "}") revert TrailingBytes(); if (_ws(body, at + 1) != body.length) revert TrailingBytes(); + + assembly ("memory-safe") { + mstore(kids, keys) + mstore(moduli, keys) + } + + // A set, so a repeat is refused rather than counted twice. Bounded by + // MAX_KEYS squared. + for (uint256 i = 1; i < keys; ++i) { + for (uint256 j = 0; j < i; ++j) { + if (moduli[i] == moduli[j]) revert DuplicateKey(moduli[i]); + } + } } /// @dev The index of the `}` closing the object opened at `open`. @@ -610,13 +608,23 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab revert UnterminatedMember("keys"); } - function _applyKey(bytes memory jwk, uint256 expiry, uint256 provenAt) private { - bytes memory kid = _member(jwk, "kid"); + /// @dev One JWK: its kid, and the limb hash of its modulus. + /// + /// The exponent is read and pinned, not stored. The `google/v1` + /// circuit verifies RS256 with the public exponent fixed at 65537, + /// and the verifier trusts a key by its modulus alone -- so a key + /// Google published under another exponent would be trusted for a + /// signature the circuit does not check. Google has only ever + /// published `AQAB` here; anything else is refused by name. + function _readKey(bytes memory jwk) private pure returns (string memory kid, bytes32 modulusHash) { + kid = string(_member(jwk, "kid")); bytes memory nB64url = _member(jwk, "n"); + bytes memory exponent = _member(jwk, "e"); + if (keccak256(exponent) != keccak256(EXPECTED_EXPONENT)) revert WrongExponent(exponent); bytes memory rawN = _b64urlDecode(nB64url); if (rawN.length != MODULUS_BYTES) revert InvalidModulusLength(); - _processClaim(kid, _modulusHash(rawN), expiry, provenAt); + modulusHash = _modulusHash(rawN); } /// @dev One string member of a flat object. The token `"name"` appears @@ -659,96 +667,60 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab return keccak256(packed); } - function _processClaim(bytes memory kid, bytes32 modulusHash, uint256 expiry, uint256 provenAt) private { + /// @dev Apply a set the notary read at `observedAt`. Three outcomes, and + /// the notary's clock alone decides the first. + /// + /// A reading dated no later than the one in force is ignored -- never + /// refused. Rotation is open and a reading binds no contract, so + /// anyone can replay any still-fresh reading anywhere, forever; + /// ignoring keeps a front-runner from bricking the honest keeper by + /// landing its reading first, and skipping keeps an older reading from + /// rolling the set back or re-stamping its lifetime. No later includes + /// equal: a second reading dated the same second cannot swap the set + /// -- the rule for equal evidence everywhere in the protocol + /// (REQ-COMMON-25A). + /// + /// A newer reading of the same set is Google saying the set still + /// stands: its lifetime restarts from this reading, nothing shifts, + /// `previous` stays as it was. A newer reading of a different set is + /// a rotation: what was current is kept one generation for the tokens + /// still in flight under it, and what was previous is gone. + function _apply(uint64 observedAt, string[] memory kids, bytes32[] memory moduli) private { GoogleJwtRootsStorage storage $ = _s(); - bytes32 kidHash = keccak256(kid); - // Ignore a claim proved no later than the one already applied -- the - // monotonic rule that makes replay harmless. Rotation is open and a - // reading binds no contract, so anyone can replay any still-fresh - // reading anywhere, forever; ignoring (never reverting) keeps a - // front-runner from bricking the honest keeper's batch by landing one - // claim from it first, and skipping (never applying) keeps an older - // reading from rolling a kid back or re-stamping its TTL. - // - // Strictly later, so equal evidence leaves the entry alone whatever it - // carries: a resubmission of the applied reading is a no-op that - // re-stamps nothing and emits nothing, and a second reading dated the - // same second cannot swap the kid's key and retire the one Google - // still signs with. That is the rule for equal evidence everywhere in - // the protocol (REQ-COMMON-25A). - if (provenAt <= $.rotatedAtKid[kidHash]) return; - bytes32 previous = $.modulusOfKid[kidHash]; - $.rotatedAtKid[kidHash] = provenAt; - - // The key this kid used to carry stops being trusted NOW, rather than - // when its thirty-day stamp runs out. - // - // The verifier resolves by modulus, not by kid, so leaving the old - // entry keeps a retired key usable for the rest of its TTL -- which is - // exactly the window a compromised key would be used in. Google - // publishes overlapping keys and tokens live about an hour, so the cost - // is that a token signed by the key Google just retired stops verifying - // a little early; the user signs in again. - if (previous != bytes32(0) && previous != modulusHash) { - delete $.trustedHashExpiresAt[previous]; - emit ModulusUntrusted(previous); - } else if (previous == bytes32(0)) { - _trackKid(kidHash); - } + if (observedAt <= $.current.observedAt) return; - $.modulusOfKid[kidHash] = modulusHash; - $.expiresAtKid[kidHash] = expiry; - $.trustedHashExpiresAt[modulusHash] = expiry; - emit ModulusRotated(kidHash, string(kid), modulusHash, expiry); - emit RootApplied(kidHash, modulusHash, provenAt, expiry); - } + if (_sameSet($.current.moduli, moduli)) { + $.current.observedAt = observedAt; + emit ReadingRefreshed(observedAt); + return; + } - /// Add a kid to the enumeration. Growth is doubly bounded: a kid can only - /// enter through a notarized reading of Google's own JWKS (a submitter - /// cannot invent one), and the set is capped -- when full, expired entries - /// are pruned to make room, and only a set full of LIVE keys refuses. - function _trackKid(bytes32 kidHash) private { - GoogleJwtRootsStorage storage $ = _s(); - if ($.trackedKidIndex[kidHash] != 0) return; - if ($.trackedKids.length >= MAX_TRACKED_KIDS) { - _pruneExpired(); - if ($.trackedKids.length >= MAX_TRACKED_KIDS) revert TooManyKids(); + $.previous.observedAt = $.current.observedAt; + $.previous.moduli = $.current.moduli; + $.current.observedAt = observedAt; + $.current.moduli = moduli; + emit KeysRotated(observedAt, kids, moduli); + } + + /// @dev Whether `stored` and `read` list the same moduli, in any order. + /// Both are sets -- a reading with a repeat is refused before it gets + /// here, and every stored generation was once a reading -- so equal + /// length plus every read modulus present is equality. Bounded by + /// MAX_KEYS squared. + function _sameSet(bytes32[] storage stored, bytes32[] memory read) private view returns (bool) { + if (stored.length != read.length) return false; + for (uint256 i = 0; i < read.length; ++i) { + if (!_lists(stored, read[i])) return false; } - $.trackedKids.push(kidHash); - $.trackedKidIndex[kidHash] = $.trackedKids.length; + return true; } - /// Swap-remove every expired kid. `rotatedAtKid` survives on purpose: it is - /// the monotonic floor that keeps a replayed old reading from resurrecting - /// the entry the prune just removed. - function _pruneExpired() private { - GoogleJwtRootsStorage storage $ = _s(); - uint256 i = $.trackedKids.length; - while (i > 0) { - i--; - bytes32 kidHash = $.trackedKids[i]; - if ($.expiresAtKid[kidHash] > block.timestamp) continue; - - bytes32 modulusHash = $.modulusOfKid[kidHash]; - delete $.modulusOfKid[kidHash]; - delete $.expiresAtKid[kidHash]; - // Only clear the verifier-facing stamp if it is genuinely spent -- - // it can sit above the kid's own expiry only if some fresher - // rotation re-trusted the same modulus under another kid. - if ($.trustedHashExpiresAt[modulusHash] <= block.timestamp) { - delete $.trustedHashExpiresAt[modulusHash]; - } - - uint256 last = $.trackedKids.length - 1; - if (i != last) { - bytes32 moved = $.trackedKids[last]; - $.trackedKids[i] = moved; - $.trackedKidIndex[moved] = i + 1; - } - $.trackedKids.pop(); - delete $.trackedKidIndex[kidHash]; - emit RootPruned(kidHash, modulusHash); + function _lists(bytes32[] storage moduli, bytes32 modulusHash) private view returns (bool) { + uint256 n = moduli.length; + for (uint256 i = 0; i < n; ++i) { + if (moduli[i] == modulusHash) return true; } + return false; } // ─── Bytes ────────────────────────────────────────────────────── diff --git a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index cd26bc60..f9abd524 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -11,6 +11,19 @@ import {INotaryService} from "../INotaryService.sol"; import {NotaryService} from "../NotaryService.sol"; import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; +/// `_splitLimbs` is internal so the hash a reading will write can be computed +/// test-side, independent of the list's state: a key's hash is asserted from +/// its bytes, never read back from the contract that wrote it. +contract LimbHasher is GoogleJwtRoots { + function hashOf(bytes memory modulus) external pure returns (bytes32) { + return keccak256(abi.encodePacked(_splitLimbs(modulus))); + } + + function hashOfB64url(string memory modulus) external pure returns (bytes32) { + return keccak256(abi.encodePacked(_splitLimbs(_b64urlDecode(bytes(modulus))))); + } +} + /// The `google/v1` profile's trust list, fed by a notarized reading of /// Google's JWKS endpoint. /// @@ -23,6 +36,7 @@ import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; contract GoogleJwtRootsTest is Test { GoogleJwtRoots roots; NotaryService notary; + LimbHasher hasher; address constant OWNER = address(0xA11CE); address keeper = makeAddr("keeper"); @@ -34,8 +48,8 @@ contract GoogleJwtRootsTest is Test { /// Google's real body, fetched 2026-09-03: pretty-printed, two keys. string constant GOOGLE_JWKS = "contracts/ceremony/test/fixtures/google-jwks.json"; - bytes32 constant GOOGLE_KID_1 = keccak256("943a3a5d7d919625a454e489b75c29adab57acba"); - bytes32 constant GOOGLE_KID_2 = keccak256("f10f87405a979c1df36df26606734f33cd85c271"); + string constant GOOGLE_KID_1 = "943a3a5d7d919625a454e489b75c29adab57acba"; + string constant GOOGLE_KID_2 = "f10f87405a979c1df36df26606734f33cd85c271"; /// The keeper's limb-hash vector. `decision::modulus_hash` in the keeper /// and `_splitLimbs` here must agree on this one, or a key the keeper @@ -63,6 +77,7 @@ contract GoogleJwtRootsTest is Test { ) ); roots = _deployRoots(INotaryService(address(notary))); + hasher = new LimbHasher(); vm.deal(address(this), 100 ether); vm.deal(keeper, 1 ether); @@ -182,6 +197,14 @@ contract GoogleJwtRootsTest is Test { return _body(_jwk(kid, _modulus(seed))); } + function _twoKeys(string memory kidA, string memory seedA, string memory kidB, string memory seedB) + private + pure + returns (bytes memory) + { + return _body(abi.encodePacked(_jwk(kidA, _modulus(seedA)), ",", _jwk(kidB, _modulus(seedB)))); + } + /// A 2048-bit modulus derived from `seed`, base64url without padding: 256 /// bytes encode to exactly 342 characters. Different seeds give different /// keys, which is the only property the caller needs. @@ -189,6 +212,12 @@ contract GoogleJwtRootsTest is Test { return string(_b64url(_bytes(seed, 256))); } + /// The limb hash the list writes for the key `seed` derives, computed + /// from the bytes rather than read back. + function _hash(string memory seed) private view returns (bytes32) { + return hasher.hashOf(_bytes(seed, 256)); + } + function _bytes(string memory seed, uint256 count) private pure returns (bytes memory out) { out = new bytes(count); for (uint256 i = 0; i < count; ++i) { @@ -227,7 +256,8 @@ contract GoogleJwtRootsTest is Test { } } - /// `count` keys, `kid-0` .. `kid-`, each with its own modulus. + /// `count` keys, `kid-0` .. `kid-`, each with its own modulus + /// (seeded by its kid). function _manyKeys(uint256 count) private pure returns (bytes memory list) { for (uint256 i = 0; i < count; ++i) { string memory kid = string.concat("kid-", vm.toString(i)); @@ -236,42 +266,96 @@ contract GoogleJwtRootsTest is Test { return _body(list); } - /// Install `kid` carrying a key derived from `seed`, observed now, and - /// return the modulus hash it wrote. - function _installKid(string memory kid, string memory seed) private returns (bytes32) { + /// Install one key `kid` derived from `seed`, observed now, and return the + /// hash it wrote. + function _install(string memory kid, string memory seed) private returns (bytes32) { _rotate(_reading(_oneKey(kid, seed))); - return roots.modulusOfKid(keccak256(bytes(kid))); + return _hash(seed); + } + + // ─── Reading the list back ────────────────────────────────────── + + function _current() private view returns (GoogleJwtRoots.Generation memory current) { + (current,) = roots.currentKeys(); + } + + function _previous() private view returns (GoogleJwtRoots.Generation memory previous) { + (, previous) = roots.currentKeys(); + } + + function _one(bytes32 a) private pure returns (bytes32[] memory out) { + out = new bytes32[](1); + out[0] = a; + } + + function _two(bytes32 a, bytes32 b) private pure returns (bytes32[] memory out) { + out = new bytes32[](2); + out[0] = a; + out[1] = b; + } + + function _names(string memory a, string memory b) private pure returns (string[] memory out) { + out = new string[](2); + out[0] = a; + out[1] = b; + } + + function _assertGeneration( + GoogleJwtRoots.Generation memory g, + uint64 observedAt, + bytes32[] memory moduli, + string memory label + ) private pure { + assertEq(g.observedAt, observedAt, string.concat(label, ": observedAt")); + assertEq(g.moduli, moduli, string.concat(label, ": moduli")); + } + + function _assertEmpty(GoogleJwtRoots.Generation memory g, string memory label) private pure { + _assertGeneration(g, 0, new bytes32[](0), label); + } + + /// The one `KeysRotated` among the recorded logs, decoded. + function _lastRotation() private returns (uint64 observedAt, string[] memory kids, bytes32[] memory moduli) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 topic = keccak256("KeysRotated(uint64,string[],bytes32[])"); + uint256 seen; + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].emitter != address(roots) || logs[i].topics[0] != topic) continue; + ++seen; + (observedAt, kids, moduli) = abi.decode(logs[i].data, (uint64, string[], bytes32[])); + } + assertEq(seen, 1, "one KeysRotated per rotation"); } // ─── What it accepts ──────────────────────────────────────────── function test_aContentLengthFramedReadingInstallsTheKeys() public { - bytes memory body = _body(abi.encodePacked(_jwk("kid-1", _modulus("one")), ",", _jwk("kid-2", _modulus("two")))); - _rotate(_reading(body)); + uint64 at = _now(); + _rotate(_reading(_twoKeys("kid-1", "one", "kid-2", "two"))); - bytes32 first = roots.modulusOfKid(keccak256("kid-1")); - bytes32 second = roots.modulusOfKid(keccak256("kid-2")); - assertTrue(first != bytes32(0), "kid-1 installed"); - assertTrue(second != bytes32(0), "kid-2 installed"); + bytes32 first = _hash("one"); + bytes32 second = _hash("two"); assertTrue(first != second, "distinct keys"); - assertEq(roots.trustedHashExpiresAt(first), block.timestamp + roots.DEFAULT_MODULUS_TTL()); - assertEq(roots.trustedHashExpiresAt(second), block.timestamp + roots.DEFAULT_MODULUS_TTL()); - assertEq(roots.expiresAtKid(keccak256("kid-1")), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + _assertGeneration(_current(), at, _two(first, second), "current"); + _assertEmpty(_previous(), "nothing precedes the first reading"); + assertEq(roots.trustedHashExpiresAt(first), at + roots.READING_LIFETIME()); + assertEq(roots.trustedHashExpiresAt(second), at + roots.READING_LIFETIME()); } /// Google serves the key set chunked. The chunk size here cuts the first /// modulus value three times, so the de-chunker is what reassembles it; /// the same body under `Content-Length` yields the same hash, which is the - /// framing being transparent. + /// framing being transparent -- the second reading refreshes the set + /// rather than rotating it. function test_aChunkedReadingInstallsTheKeys() public { bytes memory body = _oneKey("kid-1", "one"); _rotate(_attest(REQUEST, _chunked(body, 100), _now())); - bytes32 viaChunked = roots.modulusOfKid(keccak256("kid-1")); - assertTrue(viaChunked != bytes32(0), "installed through chunked framing"); + assertEq(_current().moduli, _one(_hash("one")), "installed through chunked framing"); vm.warp(block.timestamp + 1); _rotate(_reading(body)); - assertEq(roots.modulusOfKid(keccak256("kid-1")), viaChunked, "framing changed the key"); + assertEq(_current().moduli, _one(_hash("one")), "framing changed the key"); + _assertEmpty(_previous(), "framing changed the key"); } /// The document this contract exists to read: Google's body, verbatim, @@ -281,19 +365,23 @@ contract GoogleJwtRootsTest is Test { bytes memory body = bytes(vm.readFile(GOOGLE_JWKS)); assertEq(string(_slice(body, 0, 13)), "{\n \"keys\": [", "the fixture is Google's pretty-printed body"); + vm.recordLogs(); _rotate(_attest(REQUEST, _chunked(body, 512), _now())); - assertTrue(roots.modulusOfKid(GOOGLE_KID_1) != bytes32(0), "first Google key installed"); - assertTrue(roots.modulusOfKid(GOOGLE_KID_2) != bytes32(0), "second Google key installed"); - assertEq(roots.currentRoots().length, 2); + (, string[] memory kids, bytes32[] memory moduli) = _lastRotation(); + assertEq(kids, _names(GOOGLE_KID_1, GOOGLE_KID_2), "both Google kids, in Google's order"); + assertTrue(moduli[0] != moduli[1], "two distinct keys"); + assertEq(_current().moduli, moduli); assertFalse(roots.needsRotation()); } /// Cross-language pin: the keeper's `decision::modulus_hash` and the - /// Google circuit produce this value for this modulus. + /// Google circuit produce this value for this modulus. The test-side + /// hasher is pinned to the same vector, so every `_hash` below is too. function test_theLimbHashMatchesTheKeeper() public { + assertEq(hasher.hashOfB64url(KEEPER_MODULUS), KEEPER_MODULUS_HASH); _rotate(_reading(_body(_jwk("keeper", KEEPER_MODULUS)))); - assertEq(roots.modulusOfKid(keccak256("keeper")), KEEPER_MODULUS_HASH); + assertEq(_current().moduli, _one(KEEPER_MODULUS_HASH)); assertGt(roots.trustedHashExpiresAt(KEEPER_MODULUS_HASH), 0); } @@ -305,233 +393,239 @@ contract GoogleJwtRootsTest is Test { bytes memory sig = _sign(attested); vm.prank(keeper); roots.rotate{value: FEE}(attested, sig); - assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); + assertEq(_current().moduli, _one(_hash("one"))); } /// The Notary Fee is the one thing a rotation costs beyond gas, and it /// accrues where every other attestation's does. function test_theRotationPaysTheNotaryFee() public { assertEq(roots.quoteRotation(), FEE); - _installKid("kid-1", "one"); + _install("kid-1", "one"); assertEq(address(notary).balance, FEE, "the fee reached the Notary Service"); assertEq(address(roots).balance, 0, "nothing stays with the list"); } - /// Re-applying the same reading writes the same values. It must not - /// revert: with an open caller set, a revert would let a front-runner land - /// the reading first and brick the honest keeper. - function test_replayingAReadingIsIdempotent() public { + // ─── Which readings count ─────────────────────────────────────── + + /// Re-applying the reading in force writes nothing and emits nothing -- + /// in particular it does NOT restart the lifetime, so spamming one + /// reading for its whole freshness window cannot stretch trust by a + /// second. And it must not revert: with an open caller set, a revert + /// would let a front-runner land the reading first and brick the honest + /// keeper. + function test_replayingTheReadingInForceIsIgnored() public { + uint64 at = _now(); bytes memory attested = _reading(_oneKey("kid-1", "one")); _rotate(attested); - bytes32 first = roots.modulusOfKid(keccak256("kid-1")); + vm.warp(block.timestamp + 30 minutes); // still inside the window + vm.recordLogs(); _rotate(attested); - assertEq(roots.modulusOfKid(keccak256("kid-1")), first); + + assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + _assertGeneration(_current(), at, _one(_hash("one")), "current"); + _assertEmpty(_previous(), "a replay shifted the list"); + assertEq( + roots.trustedHashExpiresAt(_hash("one")), at + roots.READING_LIFETIME(), "replay restarted the lifetime" + ); } - /// Resubmitting the exact reading already applied writes nothing -- in - /// particular it does NOT re-stamp the TTL, so spamming one reading for - /// its whole freshness window cannot stretch trust by even a second. - function test_anIdenticalResubmissionDoesNotRestampTheTtl() public { - bytes memory attested = _reading(_oneKey("kid-1", "one")); - _rotate(attested); - uint256 expiry = roots.expiresAtKid(keccak256("kid-1")); + /// Two readings dated the same second cannot disagree: the second is + /// ignored whatever it carries, so equal evidence never swaps the set + /// and drops the keys Google still signs with. + function test_aReadingDatedTheSameSecondIsIgnored() public { + uint64 at = _now(); + _rotate(_readingAt(_oneKey("kid-1", "one"), at)); - vm.warp(block.timestamp + 30 minutes); // still inside the window vm.recordLogs(); - _rotate(attested); + _rotate(_readingAt(_oneKey("kid-1", "two"), at)); // must not revert, must not apply - assertEq(roots.expiresAtKid(keccak256("kid-1")), expiry, "replay re-stamped the TTL"); assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + _assertGeneration(_current(), at, _one(_hash("one")), "current"); + _assertEmpty(_previous(), "the same second shifted the list"); + assertEq(roots.trustedHashExpiresAt(_hash("two")), 0, "the same second installed a key"); } /// The rollback that matters: a GENUINELY signed older reading, replayed /// inside its freshness window after a newer one has landed. The signature - /// verifies; the per-kid monotonic stamp is what refuses the regression -- + /// verifies; the reading's own clock is what refuses the regression -- /// silently, so a keeper batch is never bricked. - function test_aGenuinelySignedOlderReadingCannotRegressState() public { - bytes memory older = _readingAt(_oneKey("kid-1", "retired-modulus"), _now() - 30 minutes); - bytes32 newModulus = _installKid("kid-1", "current-modulus"); - - bytes32 kidHash = keccak256("kid-1"); - uint256 stamp = roots.rotatedAtKid(kidHash); - uint256 expiry = roots.expiresAtKid(kidHash); + function test_aGenuinelySignedOlderReadingIsIgnored() public { + bytes memory older = _readingAt(_oneKey("kid-1", "retired"), _now() - 30 minutes); + uint64 at = _now(); + bytes32 live = _install("kid-1", "live"); + vm.recordLogs(); _rotate(older); // must not revert, must not apply - assertEq(roots.modulusOfKid(kidHash), newModulus, "older reading rolled the kid back"); - assertEq(roots.rotatedAtKid(kidHash), stamp, "older reading overwrote the stamp"); - assertEq(roots.expiresAtKid(kidHash), expiry, "older reading re-stamped the TTL"); - assertEq(roots.trustedHashExpiresAt(newModulus), expiry, "current key lost trust"); + assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + _assertGeneration(_current(), at, _one(live), "older reading rolled the set back"); + _assertEmpty(_previous(), "older reading shifted the list"); + assertEq(roots.trustedHashExpiresAt(_hash("retired")), 0, "older reading installed a retired key"); + assertEq(roots.trustedHashExpiresAt(live), at + roots.READING_LIFETIME(), "live key lost trust"); } - /// The freshest-reading mark only moves forward, whoever submits. - function test_freshestObservedAtIsMonotonic() public { - assertEq(roots.freshestObservedAt(), 0); + /// A newer reading of the set already current is Google saying the set + /// still stands: its lifetime restarts from the new reading, nothing + /// shifts, and the order Google listed the keys in is immaterial. + function test_aNewerReadingOfTheSameSetRefreshesWithoutShifting() public { uint64 first = _now(); - _rotate(_readingAt(_oneKey("kid-1", "one"), first)); - assertEq(roots.freshestObservedAt(), first); - - _rotate(_readingAt(_oneKey("kid-1", "one"), first - 10 minutes)); - assertEq(roots.freshestObservedAt(), first, "an older reading moved the mark"); + _rotate(_readingAt(_twoKeys("kid-1", "one", "kid-2", "two"), first)); + bytes32[] memory set = _two(_hash("one"), _hash("two")); vm.warp(first + 20 minutes); - _rotate(_readingAt(_oneKey("kid-1", "one"), first + 10 minutes)); - assertEq(roots.freshestObservedAt(), first + 10 minutes, "a newer reading must move the mark"); - } - - // ─── Losing trust ─────────────────────────────────────────────── - - /// The verifier resolves by modulus, not by kid, so a key that a rotation - /// replaced has to stop being trusted at that moment. Leaving it keeps a - /// retired key usable for the rest of its thirty-day stamp -- which is the - /// exact window a compromised key would be used in. - function test_rotatingAKidRetiresTheKeyItCarried() public { - bytes32 first = _installKid("kid-1", "modulus-one"); - assertGt(roots.trustedHashExpiresAt(first), 0, "first key trusted"); - - vm.warp(block.timestamp + 1 minutes); - vm.expectEmit(true, false, false, true); - emit GoogleJwtRoots.ModulusUntrusted(first); - bytes32 second = _installKid("kid-1", "modulus-two"); - - assertGt(roots.trustedHashExpiresAt(second), 0, "second key trusted"); - assertEq(roots.trustedHashExpiresAt(first), 0, "the replaced key is no longer trusted"); - } - - /// The case that cannot wait for a rotation: a key Google has not retired, - /// or has retired in a way this list has not seen yet. - function test_theOwnerCanUntrustAKeyOutright() public { - bytes32 modulusHash = _installKid("kid-1", "one"); - assertGt(roots.trustedHashExpiresAt(modulusHash), 0); - - vm.prank(OWNER); - vm.expectEmit(true, false, false, true); - emit GoogleJwtRoots.ModulusUntrusted(modulusHash); - roots.untrustModulus(modulusHash); - - assertEq(roots.trustedHashExpiresAt(modulusHash), 0, "no longer trusted"); - } - - function test_onlyTheOwnerUntrusts() public { - vm.prank(keeper); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, keeper)); - roots.untrustModulus(bytes32(uint256(1))); - } - - // ─── Expiry and pruning ───────────────────────────────────────── - - /// Time alone retires a key: past its stamp the verifier-facing read says - /// expired, with no transaction from anyone. `prune()` -- callable by - /// anyone -- then reclaims the bookkeeping. - function test_anExpiredKeyIsPrunableByAnyone() public { - uint64 provenAt = _now(); - bytes32 modulusHash = _installKid("kid-1", "one"); - bytes32 kidHash = keccak256("kid-1"); - uint256 expiry = roots.expiresAtKid(kidHash); - - vm.warp(expiry); // the stamp is spent; use-site checks already refuse it - - vm.expectEmit(true, false, false, true); - emit GoogleJwtRoots.RootPruned(kidHash, modulusHash); - vm.prank(keeper); - roots.prune(); + uint64 second = first + 10 minutes; + vm.expectEmit(false, false, false, true); + emit GoogleJwtRoots.ReadingRefreshed(second); + _rotate(_readingAt(_twoKeys("kid-2", "two", "kid-1", "one"), second)); // the same set, reversed - assertEq(roots.modulusOfKid(kidHash), bytes32(0), "kid still tracked"); - assertEq(roots.expiresAtKid(kidHash), 0); - assertEq(roots.trustedHashExpiresAt(modulusHash), 0, "spent stamp not cleared"); - assertEq(roots.currentRoots().length, 0, "enumeration not reclaimed"); - // The monotonic floor survives the prune: an old reading still cannot - // resurrect what time retired. - assertEq(roots.rotatedAtKid(kidHash), provenAt, "provenance floor lost"); + _assertGeneration(_current(), second, set, "refreshed in place, original order kept"); + _assertEmpty(_previous(), "a refresh shifted the list"); + assertEq(roots.trustedHashExpiresAt(_hash("one")), second + roots.READING_LIFETIME(), "lifetime restarted"); + assertEq(roots.trustedHashExpiresAt(_hash("two")), second + roots.READING_LIFETIME(), "lifetime restarted"); + assertEq(roots.freshestObservedAt(), second); } - /// Pruning is a no-op while the keys are alive. - function test_pruneLeavesLiveKeysAlone() public { - bytes32 modulusHash = _installKid("kid-1", "one"); + /// A newer reading of a different set is a rotation: what was current is + /// kept one generation back, the reading becomes current, and the event + /// carries the reading's keys in the order Google listed them. + function test_aNewerReadingOfADifferentSetRotates() public { + uint64 first = _now(); + bytes32 old = _install("kid-a", "a"); - vm.prank(keeper); - roots.prune(); + vm.warp(first + 20 minutes); + uint64 second = first + 10 minutes; + bytes32[] memory incoming = _two(_hash("b"), _hash("c")); + vm.expectEmit(false, false, false, true); + emit GoogleJwtRoots.KeysRotated(second, _names("kid-b", "kid-c"), incoming); + _rotate(_readingAt(_twoKeys("kid-b", "b", "kid-c", "c"), second)); - assertEq(roots.currentRoots().length, 1, "a live key was pruned"); - assertGt(roots.trustedHashExpiresAt(modulusHash), 0); + _assertGeneration(_current(), second, incoming, "current"); + _assertGeneration(_previous(), first, _one(old), "previous"); + assertEq(roots.trustedHashExpiresAt(old), first + roots.READING_LIFETIME(), "the old set keeps its own stamp"); + assertEq(roots.trustedHashExpiresAt(_hash("b")), second + roots.READING_LIFETIME()); + assertEq(roots.trustedHashExpiresAt(_hash("c")), second + roots.READING_LIFETIME()); + assertEq(roots.freshestObservedAt(), second); } - /// The tracked set is capped, and the cap defends itself: a full set first - /// sheds its expired entries, and only a set full of LIVE keys refuses. - /// Since every kid must arrive inside a notarized reading of Google's own - /// JWKS, an adversary cannot even choose the kids -- this is the ceiling - /// on Google's, not the submitter's, behavior. - function test_theTrackedSetIsBoundedAndSelfPrunes() public { - uint256 max = roots.MAX_TRACKED_KIDS(); - _rotate(_reading(_manyKeys(max))); - assertEq(roots.currentRoots().length, max, "set should sit at the cap"); + /// Two generations and no history: a third distinct set drops the oldest + /// outright, and a key only that set listed stops being trusted -- with + /// no prune, no untrust, no transaction but the rotation itself. + function test_aThirdSetDropsTheOldest() public { + uint64 first = _now(); + bytes32 a = _install("kid-a", "a"); + vm.warp(first + 1 minutes); + uint64 second = _now(); + bytes32 b = _install("kid-b", "b"); + vm.warp(second + 1 minutes); + uint64 third = _now(); + bytes32 c = _install("kid-c", "c"); + + _assertGeneration(_current(), third, _one(c), "current"); + _assertGeneration(_previous(), second, _one(b), "previous"); + assertEq(roots.trustedHashExpiresAt(a), 0, "the oldest set is still trusted"); + assertEq(roots.trustedHashExpiresAt(b), second + roots.READING_LIFETIME()); + assertEq(roots.trustedHashExpiresAt(c), third + roots.READING_LIFETIME()); + } + + /// Google's sets overlap -- a key is usually in the reading before and + /// the reading after -- and a key in both generations carries the current + /// one's stamp: the later reading is the later word that it is listed. + function test_aKeyInBothGenerationsCarriesTheCurrentStamp() public { + uint64 first = _now(); + _rotate(_readingAt(_twoKeys("kid-1", "one", "kid-2", "two"), first)); + vm.warp(first + 1 minutes); + uint64 second = _now(); + _rotate(_readingAt(_twoKeys("kid-2", "two", "kid-3", "three"), second)); + + assertEq(roots.trustedHashExpiresAt(_hash("two")), second + roots.READING_LIFETIME(), "in both: current stamp"); + assertEq(roots.trustedHashExpiresAt(_hash("one")), first + roots.READING_LIFETIME(), "previous only"); + assertEq(roots.trustedHashExpiresAt(_hash("three")), second + roots.READING_LIFETIME(), "current only"); + } + + /// Trust lapses by the reading's own clock, with no transaction from + /// anyone: the stamp is `createdAt + READING_LIFETIME` -- the notary's + /// time, not the block the rotation landed in -- and past it the + /// verifier-facing read is already in the past. + function test_trustLapsesByTheReadingsOwnClockWithNoCall() public { + uint64 observedAt = _now() - 30 minutes; + _rotate(_readingAt(_oneKey("kid-1", "one"), observedAt)); + uint256 stamp = observedAt + roots.READING_LIFETIME(); + assertEq(roots.trustedHashExpiresAt(_hash("one")), stamp, "anchored to the reading, not the block"); + assertTrue(stamp < block.timestamp + roots.READING_LIFETIME(), "block time did not stretch it"); + + vm.warp(stamp); + assertEq(roots.trustedHashExpiresAt(_hash("one")), stamp, "nothing rewrote the stamp"); + assertTrue(roots.trustedHashExpiresAt(_hash("one")) <= block.timestamp, "expired by time alone"); + assertTrue(roots.needsRotation()); + _assertGeneration(_current(), observedAt, _one(_hash("one")), "the generation is still what was read"); + } + + /// The freshest-reading mark is the current generation's clock: it only + /// ever moves forward, whether the newer reading refreshes or rotates. + function test_freshestObservedAtIsTheReadingInForce() public { + assertEq(roots.freshestObservedAt(), 0); + uint64 first = _now(); + _rotate(_readingAt(_oneKey("kid-1", "one"), first)); + assertEq(roots.freshestObservedAt(), first); - // A brand-new kid while every slot holds a live key: refused. - _refuses(_reading(_oneKey("kid-overflow", "overflow")), GoogleJwtRoots.TooManyKids.selector); + _rotate(_readingAt(_oneKey("kid-1", "one"), first - 10 minutes)); + assertEq(roots.freshestObservedAt(), first, "an older reading moved the mark"); - // Once the old stamps are spent, the same insert prunes its own room. - vm.warp(block.timestamp + roots.DEFAULT_MODULUS_TTL() + 1); - _rotate(_reading(_oneKey("kid-overflow", "overflow"))); - assertEq(roots.currentRoots().length, 1, "expired entries should have been shed"); - assertEq(roots.currentRoots()[0].kidHash, keccak256("kid-overflow")); + vm.warp(first + 20 minutes); + _rotate(_readingAt(_oneKey("kid-1", "one"), first + 10 minutes)); // same set: refresh + assertEq(roots.freshestObservedAt(), first + 10 minutes, "a refresh must move the mark"); + _rotate(_readingAt(_oneKey("kid-2", "two"), first + 15 minutes)); // different set: rotation + assertEq(roots.freshestObservedAt(), first + 15 minutes, "a rotation must move the mark"); } // ─── What a keeper reads ──────────────────────────────────────── - /// One call, the whole state: every tracked key with its provenance and - /// expiry. - function test_currentRootsDescribesTheWholeList() public { - uint64 provenAt = _now() - 5 minutes; - _rotate(_readingAt(_manyKeys(3), provenAt)); - - GoogleJwtRoots.RootInfo[] memory infos = roots.currentRoots(); - assertEq(infos.length, 3, "every key should be listed"); - for (uint256 i = 0; i < infos.length; ++i) { - assertEq(infos[i].kidHash, keccak256(bytes(string.concat("kid-", vm.toString(i))))); - assertEq(infos[i].modulusHash, roots.modulusOfKid(infos[i].kidHash)); - assertEq(infos[i].observedAt, provenAt, "provenance is the reading's timestamp"); - assertEq(infos[i].expiresAt, roots.trustedHashExpiresAt(infos[i].modulusHash)); - } + /// Before any reading: two empty generations, a zero mark, and a list + /// that wants a rotation. + function test_currentKeysStartsEmpty() public view { + _assertEmpty(_current(), "current"); + _assertEmpty(_previous(), "previous"); + assertEq(roots.freshestObservedAt(), 0); + assertTrue(roots.needsRotation()); } /// The single bit a keeper polls: true until the first rotation, false - /// while a key has RENEWAL_MARGIN of trusted runway, true again as the - /// runway shortens -- and long before anything actually expires. + /// while the current generation has RENEWAL_MARGIN of trusted runway, + /// true again as the runway shortens -- and long before it expires. function test_needsRotationTracksTheTrustedRunway() public { assertTrue(roots.needsRotation(), "an empty list needs a rotation"); - _installKid("kid-1", "one"); + uint64 at = _now(); + _install("kid-1", "one"); assertFalse(roots.needsRotation(), "a fresh rotation buys quiet"); - uint256 expiry = roots.expiresAtKid(keccak256("kid-1")); - vm.warp(expiry - roots.RENEWAL_MARGIN()); + uint256 stamp = at + roots.READING_LIFETIME(); + vm.warp(stamp - roots.RENEWAL_MARGIN() - 1); + assertFalse(roots.needsRotation(), "still a second of margin to spare"); + vm.warp(stamp - roots.RENEWAL_MARGIN()); assertTrue(roots.needsRotation(), "the margin should trip before expiry does"); } - /// Each applied key announces itself with the reading's own timestamp -- - /// the notary's clock, not the block's -- so a bot can follow the list - /// from logs alone. - function test_rotationEmitsRootAppliedWithProvenance() public { + /// The rotation announces the reading's keys -- kids and limb hashes in + /// the order Google listed them -- with the reading's own timestamp, the + /// notary's clock, so a bot can follow the list from logs alone. + function test_aRotationAnnouncesTheKeysInReadingOrder() public { uint64 provenAt = _now() - 10 minutes; - bytes32 kidHash = keccak256("kid-1"); vm.recordLogs(); - _rotate(_readingAt(_oneKey("kid-1", "one"), provenAt)); + _rotate(_readingAt(_manyKeys(3), provenAt)); - Vm.Log[] memory logs = vm.getRecordedLogs(); - bytes32 topic = keccak256("RootApplied(bytes32,bytes32,uint256,uint256)"); - uint256 seen; - for (uint256 i = 0; i < logs.length; ++i) { - if (logs[i].emitter != address(roots) || logs[i].topics[0] != topic) continue; - ++seen; - assertEq(logs[i].topics[1], kidHash); - assertEq(logs[i].topics[2], roots.modulusOfKid(kidHash)); - (uint256 observedAt, uint256 expiresAt) = abi.decode(logs[i].data, (uint256, uint256)); - assertEq(observedAt, provenAt, "observedAt is the notary's clock"); - assertEq(expiresAt, block.timestamp + roots.DEFAULT_MODULUS_TTL()); + (uint64 observedAt, string[] memory kids, bytes32[] memory moduli) = _lastRotation(); + assertEq(observedAt, provenAt, "observedAt is the notary's clock"); + assertEq(kids.length, 3); + assertEq(moduli.length, 3); + for (uint256 i = 0; i < 3; ++i) { + string memory kid = string.concat("kid-", vm.toString(i)); + assertEq(kids[i], kid, "kid in reading order"); + assertEq(moduli[i], _hash(kid), "modulus in reading order"); } - assertEq(seen, 1, "one RootApplied per key"); + assertEq(_current().moduli, moduli, "the event is what was stored"); } // ─── Administration ───────────────────────────────────────────── @@ -565,7 +659,7 @@ contract GoogleJwtRootsTest is Test { // ...and the new one's does, at the new fee. roots.rotate{value: 2 * FEE}(attested, _signWith(otherKey, attested)); - assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); + assertEq(_current().moduli, _one(_hash("one"))); assertEq(address(other).balance, 2 * FEE); } @@ -765,6 +859,17 @@ contract GoogleJwtRootsTest is Test { _refuses(_withRequest(request), GoogleJwtRoots.WrongRequestLine.selector); } + /// The same path with a query is another document. Google really serves + /// `/oauth2/v3/certs?callback=` as a 200 `text/javascript` JSONP + /// body -- `({...})` -- that begins with bytes the requester chose, + /// so the whole request line through its CRLF is pinned, and a query is + /// refused before any byte of the response is read. + function test_refusesAQueryInTheRequestTarget() public { + bytes memory request = + abi.encodePacked("GET /oauth2/v3/certs?callback=foo HTTP/1.1\r\n", _slice(REQUEST, 31, REQUEST.length)); + _refuses(_withRequest(request), GoogleJwtRoots.WrongRequestLine.selector); + } + /// Only the origin-form line, like every other verifier. function test_refusesAnAbsoluteFormRequestTarget() public { bytes memory request = abi.encodePacked( @@ -890,6 +995,23 @@ contract GoogleJwtRootsTest is Test { _refuses(_withResponse(response), GoogleJwtRoots.NoHeadBoundary.selector); } + /// The prover negotiates no compression, so a `Content-Encoding` is a + /// body this contract cannot read. Refused by name, before the framing + /// is even looked at, rather than as some byte deep in the JSON walk. + function test_refusesAnEncodedBody() public { + bytes memory body = _oneKey("kid-1", "one"); + bytes memory response = abi.encodePacked( + STATUS_OK, + CONTENT_TYPE, + "content-encoding: gzip\r\n", + "content-length: ", + vm.toString(body.length), + "\r\n\r\n", + body + ); + _refuses(_withResponse(response), GoogleJwtRoots.EncodedBody.selector); + } + /// Every deviation from strict chunked framing is one error: a size that /// is not hex, a chunk extension, a missing terminator, trailers, an /// over-long size, and chunk data not followed by CRLF. @@ -961,7 +1083,7 @@ contract GoogleJwtRootsTest is Test { function test_acceptsABodyDelimitedByConnectionClose() public { bytes memory response = abi.encodePacked(STATUS_OK, CONTENT_TYPE, "\r\n", _oneKey("kid-1", "one")); _rotate(_withResponse(response)); - assertTrue(roots.modulusOfKid(keccak256("kid-1")) != bytes32(0)); + assertEq(_current().moduli, _one(_hash("one"))); } // ─── What it refuses: the key set ─────────────────────────────── @@ -1001,6 +1123,23 @@ contract GoogleJwtRootsTest is Test { _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "kid")); } + /// The circuit verifies RS256 under 65537 and nothing else, and the + /// verifier trusts a key by its modulus alone -- so a key published with + /// any other exponent must never be listed, and one that names no + /// exponent is not a key this list can vouch for. + function test_refusesAnotherExponent() public { + bytes memory body = _body(abi.encodePacked('{"kid":"kid-1","n":"', _modulus("one"), '","e":"AQAC"}')); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.WrongExponent.selector, bytes("AQAC"))); + // 65537 in a padded or longer encoding is still not the token pinned. + body = _body(abi.encodePacked('{"kid":"kid-1","n":"', _modulus("one"), '","e":"AQAB="}')); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.WrongExponent.selector, bytes("AQAB="))); + } + + function test_refusesAKeyWithoutAnExponent() public { + bytes memory body = _body(abi.encodePacked('{"kty":"RSA","kid":"kid-1","n":"', _modulus("one"), '"}')); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.MissingMember.selector, "e")); + } + /// Two `kid` members in one object: a duplicate leaves the value /// undefined, and different parsers pick different ones. function test_refusesADuplicateKidMember() public { @@ -1045,9 +1184,21 @@ contract GoogleJwtRootsTest is Test { _refuses(_withBody(body), GoogleJwtRoots.UnexpectedNesting.selector); } - /// The cap on tracked kids is also the cap on keys in one reading. - function test_refusesSeventeenKeys() public { - _refuses(_withBody(_manyKeys(17)), GoogleJwtRoots.TooManyKids.selector); + /// One reading carries at most MAX_KEYS keys, and the refusal names how + /// many it carried. The cap itself is accepted. + function test_refusesNineKeys() public { + uint256 max = roots.MAX_KEYS(); + assertEq(max, 8); + _refuses(_withBody(_manyKeys(max + 1)), abi.encodeWithSelector(GoogleJwtRoots.TooManyKeys.selector, max + 1)); + _rotate(_withBody(_manyKeys(max))); + assertEq(_current().moduli.length, max, "the cap itself is a valid reading"); + } + + /// A set, not a list: the same modulus twice is not the document Google + /// publishes, and a set with a repeat would compare as a set it is not. + function test_refusesADuplicateModulus() public { + bytes memory body = _twoKeys("kid-1", "one", "kid-2", "one"); + _refuses(_withBody(body), abi.encodeWithSelector(GoogleJwtRoots.DuplicateKey.selector, _hash("one"))); } function test_refusesTrailingBytes() public { @@ -1077,31 +1228,13 @@ contract GoogleJwtRootsTest is Test { } /// A reading with no keys says nothing about what Google publishes. It - /// would pay the fee, move `freshestObservedAt` and emit nothing. + /// would pay the fee and shift the live set into `previous` on the + /// strength of a document that names no key. function test_refusesAnEmptyKeySet() public { _refuses(_withBody('{"keys":[]}'), GoogleJwtRoots.EmptyKeySet.selector); _refuses(_withBody('{"keys": [ ]}'), GoogleJwtRoots.EmptyKeySet.selector); } - /// Two readings dated the same second cannot disagree about a kid: the - /// second is ignored whatever it carries, so equal evidence never swaps a - /// key and retires the one Google still signs with. - function test_aReadingDatedTheSameSecondCannotSwapAKey() public { - uint64 at = _now(); - _rotate(_readingAt(_oneKey("kid-1", "one"), at)); - bytes32 kidHash = keccak256("kid-1"); - bytes32 installed = roots.modulusOfKid(kidHash); - uint256 expiry = roots.expiresAtKid(kidHash); - - vm.recordLogs(); - _rotate(_readingAt(_oneKey("kid-1", "two"), at)); // must not revert, must not apply - - assertEq(roots.modulusOfKid(kidHash), installed, "the same second swapped the key"); - assertEq(roots.trustedHashExpiresAt(installed), expiry, "the installed key lost trust"); - assertEq(roots.rotatedAtKid(kidHash), at, "the stamp moved"); - assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); - } - /// The header the body is located by gets the rule every other read /// gets: a delimiter that matches twice is refused, not chosen from. function test_refusesDuplicateOrConflictingFramingHeaders() public { diff --git a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol index 37e63eec..08b9e272 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -import {Test} from "forge-std/Test.sol"; +import {Test, Vm} from "forge-std/Test.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; @@ -195,31 +195,39 @@ contract GoogleJwtRootsRealSessionTest is Test { // ─── The real rotation ────────────────────────────────────────── /// The real bytes, the real signature, a real Notary Service trusting the - /// key that signed: every kid Google published that minute is installed, - /// stamped with the notary's clock, and the fee lands where every other - /// attestation's does. + /// key that signed: every kid Google published that minute is installed + /// as the current generation, trusted for the lifetime from the notary's + /// clock, and the fee lands where every other attestation's does. function test_theRealReadingInstallsEveryKidGoogleServed() public { + vm.recordLogs(); roots.rotate{value: FEE}(record, signature); bytes memory json = _realBody(); string[] memory kids = _kids(); assertEq(_count(json, "\"kid\""), kids.length, "the list above names every kid in the body"); - - bytes32[] memory moduli = new bytes32[](kids.length); for (uint256 i = 0; i < kids.length; ++i) { assertTrue(_contains(json, abi.encodePacked("\"kid\": \"", kids[i], "\"")), "kid is in the body"); - bytes32 kidHash = keccak256(bytes(kids[i])); - moduli[i] = roots.modulusOfKid(kidHash); + } + + // The rotation announces the reading's keys in the order Google + // listed them, so the event is what ties each kid to its modulus. + (uint64 observedAt, string[] memory announced, bytes32[] memory moduli) = _rotation(); + assertEq(observedAt, createdAt, "stamped with the notary's clock"); + assertEq(announced, kids, "every kid, in the fixture's order"); + assertEq(moduli.length, kids.length); + uint256 stamp = createdAt + roots.READING_LIFETIME(); + for (uint256 i = 0; i < moduli.length; ++i) { assertTrue(moduli[i] != bytes32(0), "kid installed"); - assertEq(roots.rotatedAtKid(kidHash), createdAt, "stamped with the notary's clock"); - assertEq(roots.expiresAtKid(kidHash), block.timestamp + roots.DEFAULT_MODULUS_TTL()); - assertEq(roots.trustedHashExpiresAt(moduli[i]), block.timestamp + roots.DEFAULT_MODULUS_TTL()); + assertEq(roots.trustedHashExpiresAt(moduli[i]), stamp, "trusted for the lifetime, from the reading's clock"); for (uint256 j = 0; j < i; ++j) { assertTrue(moduli[i] != moduli[j], "four distinct keys"); } } - assertEq(roots.currentRoots().length, kids.length); + (GoogleJwtRoots.Generation memory current, GoogleJwtRoots.Generation memory previous) = roots.currentKeys(); + assertEq(current.observedAt, createdAt); + assertEq(current.moduli, moduli, "four moduli, in the fixture's order"); + assertEq(previous.moduli.length, 0, "nothing precedes the first reading"); assertFalse(roots.needsRotation()); assertEq(roots.freshestObservedAt(), createdAt); assertEq(address(notary).balance, FEE, "the fee reached the Notary Service"); @@ -250,12 +258,10 @@ contract GoogleJwtRootsRealSessionTest is Test { GoogleJwtRoots again = _deployRoots(); again.rotate{value: FEE}(reread, _signWith(TEST_KEY, reread)); - string[] memory kids = _kids(); - for (uint256 i = 0; i < kids.length; ++i) { - bytes32 kidHash = keccak256(bytes(kids[i])); - assertEq(again.modulusOfKid(kidHash), roots.modulusOfKid(kidHash), "framing changed the key"); - } - assertEq(again.currentRoots().length, kids.length); + (GoogleJwtRoots.Generation memory viaChunked,) = roots.currentKeys(); + (GoogleJwtRoots.Generation memory viaLength,) = again.currentKeys(); + assertEq(viaLength.moduli.length, _kids().length); + assertEq(viaLength.moduli, viaChunked.moduli, "framing changed the key"); } // ─── The ways to cheat with it ────────────────────────────────── @@ -305,7 +311,8 @@ contract GoogleJwtRootsRealSessionTest is Test { vm.warp(createdAt + window); roots.rotate{value: FEE}(record, signature); - assertEq(roots.currentRoots().length, 4); + (GoogleJwtRoots.Generation memory current,) = roots.currentKeys(); + assertEq(current.moduli.length, 4); } /// The real request with a second `Host` smuggled in behind a bare LF -- @@ -346,6 +353,19 @@ contract GoogleJwtRootsRealSessionTest is Test { return _dechunk(_afterHead(this.decode(record).received.revealed[0].value)); } + /// The one `KeysRotated` among the recorded logs, decoded. + function _rotation() private returns (uint64 observedAt, string[] memory kids, bytes32[] memory moduli) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + bytes32 topic = keccak256("KeysRotated(uint64,string[],bytes32[])"); + uint256 seen; + for (uint256 i = 0; i < logs.length; ++i) { + if (logs[i].emitter != address(roots) || logs[i].topics[0] != topic) continue; + ++seen; + (observedAt, kids, moduli) = abi.decode(logs[i].data, (uint64, string[], bytes32[])); + } + assertEq(seen, 1, "one KeysRotated per rotation"); + } + function _signWith(uint256 key, bytes memory attested) private pure returns (bytes memory) { bytes32 ethHash = MessageHashUtils.toEthSignedMessageHash(keccak256(attested)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, ethHash); diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index 7835ed40..1993a970 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -255,22 +255,32 @@ contract UpgradeSafetyTest is Test { vm.expectRevert(Initializable.InvalidInitialization.selector); r.initialize(OWNER, ns); - // A rotation lands BEFORE the upgrade, so what has to survive is real - // state in the namespaced slots rather than the empty list initialize - // left behind. + // Two rotations land BEFORE the upgrade -- two different one-key + // readings, so BOTH generations hold state -- and what has to survive + // is real state in the namespaced slots rather than the empty list + // initialize left behind. uint256 notaryKey = 0xA11CE5; vm.prank(OWNER); NotaryService(address(ns)).setNotary(vm.addr(notaryKey), true); - bytes memory attested = _jwksReading(uint64(vm.getBlockTimestamp())); - (uint8 v, bytes32 sigR, bytes32 sigS) = - vm.sign(notaryKey, keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(attested)))); vm.deal(address(this), 1 ether); - r.rotate{value: 7}(attested, abi.encodePacked(sigR, sigS, v)); - bytes32 kidHash = keccak256("k"); - bytes32 modulus = r.modulusOfKid(kidHash); - assertTrue(modulus != bytes32(0), "the rotation landed"); - uint256 stamp = r.rotatedAtKid(kidHash); - uint256 expiry = r.expiresAtKid(kidHash); + vm.warp(1_770_000_000); + uint64 first = uint64(vm.getBlockTimestamp()); + bytes memory attested = _jwksReading(first, JWKS_MODULUS); + r.rotate{value: 7}(attested, _signed(notaryKey, attested)); + vm.warp(first + 60); + uint64 second = uint64(vm.getBlockTimestamp()); + attested = _jwksReading(second, _otherModulus()); + r.rotate{value: 7}(attested, _signed(notaryKey, attested)); + + (GoogleJwtRoots.Generation memory current, GoogleJwtRoots.Generation memory previous) = r.currentKeys(); + assertEq(current.observedAt, second, "the second rotation landed"); + assertEq(previous.observedAt, first, "the first rotation was kept"); + assertEq(current.moduli.length, 1); + assertEq(previous.moduli.length, 1); + bytes32 newer = current.moduli[0]; + bytes32 older = previous.moduli[0]; + assertTrue(newer != older, "two different keys"); + uint256 lifetime = r.READING_LIFETIME(); GoogleJwtRoots impl2 = new GoogleJwtRoots(); _expectNotOwner(); @@ -281,24 +291,44 @@ contract UpgradeSafetyTest is Test { assertEq(r.notaryService(), address(ns)); assertEq(r.quoteRotation(), 7); assertEq(r.owner(), OWNER); - assertEq(r.modulusOfKid(kidHash), modulus, "the rotated key was lost"); - assertEq(r.trustedHashExpiresAt(modulus), expiry, "the trust stamp was lost"); - assertEq(r.rotatedAtKid(kidHash), stamp, "the provenance floor was lost"); - assertEq(r.freshestObservedAt(), stamp); - assertEq(r.currentRoots().length, 1, "the tracked set was lost"); + assertEq(r.trustedHashExpiresAt(newer), second + lifetime, "the current generation was lost"); + assertEq(r.trustedHashExpiresAt(older), first + lifetime, "the previous generation was lost"); + (current, previous) = r.currentKeys(); + assertEq(current.observedAt, second); + assertEq(current.moduli.length, 1); + assertEq(current.moduli[0], newer); + assertEq(previous.observedAt, first); + assertEq(previous.moduli.length, 1); + assertEq(previous.moduli[0], older); + assertEq(r.freshestObservedAt(), second); assertFalse(r.needsRotation()); } + function _signed(uint256 key, bytes memory attested) internal pure returns (bytes memory) { + (uint8 v, bytes32 sigR, bytes32 sigS) = + vm.sign(key, keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", keccak256(attested)))); + return abi.encodePacked(sigR, sigS, v); + } + /// One notarized reading of Google's JWKS carrying one key: the request /// and the response revealed whole, nothing committed. - function _jwksReading(uint64 createdAt) internal pure returns (bytes memory) { + function _jwksReading(uint64 createdAt, string memory modulus) internal pure returns (bytes memory) { bytes memory sent = "GET /oauth2/v3/certs HTTP/1.1\r\nhost: www.googleapis.com\r\nconnection: close\r\n\r\n"; - bytes memory body = abi.encodePacked('{"keys":[{"kid":"k","n":"', JWKS_MODULUS, '"}]}'); + bytes memory body = abi.encodePacked('{"keys":[{"kid":"k","n":"', modulus, '","e":"AQAB"}]}'); bytes memory received = abi.encodePacked("HTTP/1.1 200 OK\r\ncontent-length: ", vm.toString(body.length), "\r\n\r\n", body); return AttestationBuilder.encode(keccak256("www.googleapis.com"), createdAt, _whole(sent), _whole(received)); } + /// A second 2048-bit modulus: the keeper's vector with its first + /// character changed -- still 342 characters of base64url, a different + /// key. + function _otherModulus() internal pure returns (string memory) { + bytes memory modulus = bytes(JWKS_MODULUS); + modulus[0] = "C"; + return string(modulus); + } + function _whole(bytes memory transcript) internal pure returns (AttestationBuilder.Direction memory) { return AttestationBuilder.Direction({ revealed: AttestationBuilder.one(AttestationBuilder.Range({start: 0, value: transcript})), diff --git a/ts/packages/contracts/README.md b/ts/packages/contracts/README.md index 5f261554..be736525 100644 --- a/ts/packages/contracts/README.md +++ b/ts/packages/contracts/README.md @@ -34,10 +34,11 @@ const owner = await client.readContract({ args: [platformId, 'alice'], }) -const roots = await client.readContract({ +// The two generations of Google's key set the list holds. +const [current, previous] = await client.readContract({ address: GOOGLE_JWT_ROOTS, abi: googleJwtRootsAbi, - functionName: 'currentRoots', + functionName: 'currentKeys', }) ``` From 47989119708b2d8a88e55b912ea28e87c87350a6 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 12:37:08 +0100 Subject: [PATCH 09/15] fix(ceremony): an empty list asks for its first reading at any block time needsRotation answered the empty case by arithmetic on a zero stamp, which comes out true on a live chain and false on a chain younger than the reading lifetime. Say it outright instead. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- solidity/contracts/ceremony/GoogleJwtRoots.sol | 6 +++++- solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index f4584b87..e819ba54 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -350,7 +350,11 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// decide whether to fetch a fresh reading. True from deployment /// until the first rotation lands. function needsRotation() external view returns (bool) { - return block.timestamp + RENEWAL_MARGIN >= uint256(_s().current.observedAt) + READING_LIFETIME; + uint64 observedAt = _s().current.observedAt; + // Nothing has been read yet: true by definition, not by arithmetic on + // a zero stamp that happens to come out right on a live chain. + if (observedAt == 0) return true; + return block.timestamp + RENEWAL_MARGIN >= uint256(observedAt) + READING_LIFETIME; } // ─── The transcript ───────────────────────────────────────────── diff --git a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index f9abd524..609260ec 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -595,6 +595,11 @@ contract GoogleJwtRootsTest is Test { /// true again as the runway shortens -- and long before it expires. function test_needsRotationTracksTheTrustedRunway() public { assertTrue(roots.needsRotation(), "an empty list needs a rotation"); + // Whatever the clock says: a fresh deployment on a chain younger than + // the lifetime must still ask for its first reading. + vm.warp(1); + assertTrue(roots.needsRotation(), "an empty list needs a rotation at any block time"); + vm.warp(T0 + 10); uint64 at = _now(); _install("kid-1", "one"); From e3ec94e27f404af75a0bc73bf01f23b3cf223a98 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:01:22 +0100 Subject: [PATCH 10/15] fix(ceremony)!: a reading that is not news is refused, not ignored rotate() returned silently when a reading was dated no later than the one in force. That charged the honest keeper the Notary Fee for nothing done and gave it no signal; a front-runner landing the same reading first made the keeper's transaction succeed empty. Refuse it instead -- NotNewer(createdAt, observedAt), both clocks -- so the revert rolls the fee back with the state and the keeper loses gas only. "Never revert, or a front-runner could brick the keeper" was the rule for the per-claim nullifier design, where the first submission consumed the claim. Nothing is consumed here: the front-runner's reading IS the keeper's, applied, so the keeper's transaction has nothing left to do and its next reading lands as usual. The freshness checks still run first, so a stale or future record is named as such. Tests: the replay, same-second and older-reading cases expect NotNewer with exact arguments and still assert the stamps; a new front-run test shows the keeper's fee coming back and its next reading landing. The Rust binding's doc follows. The ABI gains an error, so the vendored artifacts and the TS ABI are regenerated (both gitignored). Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/src/bindings/ceremony.rs | 5 +- .../contracts/ceremony/GoogleJwtRoots.sol | 39 ++++++--- .../ceremony/test/GoogleJwtRoots.t.sol | 82 +++++++++++++------ 3 files changed, 88 insertions(+), 38 deletions(-) diff --git a/rust/contracts/src/bindings/ceremony.rs b/rust/contracts/src/bindings/ceremony.rs index 1f07e5c9..3ba15f4e 100644 --- a/rust/contracts/src/bindings/ceremony.rs +++ b/rust/contracts/src/bindings/ceremony.rs @@ -135,8 +135,9 @@ mod google_jwt_roots_inner { /// Permissionless. `attestedData` is the ceremony-common section /// 9.1 record of the JWKS session, `proof` the notary's /// authentication of it (a 65-byte EIP-191 signature today). A - /// reading dated no later than the current generation is - /// ignored, not refused. + /// reading dated no later than the current generation is refused + /// with `NotNewer(createdAt, observedAt)`, and the revert hands + /// the fee back. function rotate(bytes calldata attestedData, bytes calldata proof) external payable; /// What `GooglePlatformVerifier` reads: modulus hash -> when it diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index e819ba54..e5d40d13 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -50,8 +50,9 @@ import {INotaryService} from "./INotaryService.sol"; /// front-runner could consume a reading's digest and brick the honest /// keeper. Freshness comes from the notary's signed `createdAt` plus the /// window below, and a reading dated no later than the one in force is -/// ignored rather than refused. A rotation pays the Notary Fee, which is -/// the one thing a submitter needs beyond gas. +/// refused (`NotNewer`). A rotation pays the Notary Fee, which is the one +/// thing a submitter needs beyond gas -- and a refused one pays nothing, +/// since the revert rolls the fee back with the state. /// /// **The reading names no contract.** The attested record is the TLS /// session alone -- authority, notary clock, transcript -- so the same @@ -184,6 +185,12 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab error WrongAuthority(bytes32 expected, bytes32 found); error FutureProof(); error StaleProof(); + /// @dev Dated no later than the reading in force: a replay of it, or an + /// older reading landing after a newer one. Refused, not ignored, so + /// the Notary Fee rolls back with the state and a keeper that was + /// front-run learns it from the revert rather than from a fee paid + /// for nothing done. + error NotNewer(uint64 createdAt, uint64 observedAt); /// @dev A direction carries a commitment. The reading must hide nothing: /// a hidden byte is a byte this contract reads around, and the join /// below is only safe when there is nothing to read around. @@ -674,15 +681,23 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// @dev Apply a set the notary read at `observedAt`. Three outcomes, and /// the notary's clock alone decides the first. /// - /// A reading dated no later than the one in force is ignored -- never - /// refused. Rotation is open and a reading binds no contract, so - /// anyone can replay any still-fresh reading anywhere, forever; - /// ignoring keeps a front-runner from bricking the honest keeper by - /// landing its reading first, and skipping keeps an older reading from - /// rolling the set back or re-stamping its lifetime. No later includes - /// equal: a second reading dated the same second cannot swap the set - /// -- the rule for equal evidence everywhere in the protocol - /// (REQ-COMMON-25A). + /// A reading dated no later than the one in force is refused, + /// `NotNewer`. Rotation is open and a reading binds no contract, so + /// anyone can replay any still-fresh reading anywhere, forever; the + /// refusal keeps an older reading from rolling the set back and a + /// replay from re-stamping the lifetime. It used to be a silent + /// return, on the rule that a permissionless path must never revert + /// or a front-runner could brick the honest keeper by landing first. + /// That rule belonged to the per-claim nullifier design, where the + /// first submission consumed the claim; here nothing is consumed -- + /// the front-runner's reading IS the keeper's, applied -- so the + /// keeper's transaction has nothing left to do, and a revert says so + /// while rolling the Notary Fee back with the state. The return + /// charged the fee for nothing done and gave the keeper no signal. + /// No later includes equal: a second reading dated the same second + /// cannot swap the set -- the rule for equal evidence everywhere in + /// the protocol (REQ-COMMON-25A). The freshness checks in `rotate` + /// run first, so a stale or future record is named as such. /// /// A newer reading of the same set is Google saying the set still /// stands: its lifetime restarts from this reading, nothing shifts, @@ -691,7 +706,7 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// still in flight under it, and what was previous is gone. function _apply(uint64 observedAt, string[] memory kids, bytes32[] memory moduli) private { GoogleJwtRootsStorage storage $ = _s(); - if (observedAt <= $.current.observedAt) return; + if (observedAt <= $.current.observedAt) revert NotNewer(observedAt, $.current.observedAt); if (_sameSet($.current.moduli, moduli)) { $.current.observedAt = observedAt; diff --git a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index 609260ec..6ae1406b 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -407,22 +407,20 @@ contract GoogleJwtRootsTest is Test { // ─── Which readings count ─────────────────────────────────────── - /// Re-applying the reading in force writes nothing and emits nothing -- - /// in particular it does NOT restart the lifetime, so spamming one - /// reading for its whole freshness window cannot stretch trust by a - /// second. And it must not revert: with an open caller set, a revert - /// would let a front-runner land the reading first and brick the honest - /// keeper. - function test_replayingTheReadingInForceIsIgnored() public { + /// Re-applying the reading in force is refused: `NotNewer`, with the + /// reading's own clock on both sides. Nothing is written -- in particular + /// the lifetime does NOT restart, so spamming one reading for its whole + /// freshness window cannot stretch trust by a second -- and the revert + /// hands the fee back, where a silent return charged it for nothing done. + function test_replayingTheReadingInForceIsRefused() public { uint64 at = _now(); bytes memory attested = _reading(_oneKey("kid-1", "one")); _rotate(attested); vm.warp(block.timestamp + 30 minutes); // still inside the window - vm.recordLogs(); - _rotate(attested); + _refuses(attested, abi.encodeWithSelector(GoogleJwtRoots.NotNewer.selector, at, at)); - assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); + assertEq(address(notary).balance, FEE, "the refused replay paid a fee"); _assertGeneration(_current(), at, _one(_hash("one")), "current"); _assertEmpty(_previous(), "a replay shifted the list"); assertEq( @@ -431,16 +429,16 @@ contract GoogleJwtRootsTest is Test { } /// Two readings dated the same second cannot disagree: the second is - /// ignored whatever it carries, so equal evidence never swaps the set + /// refused whatever it carries, so equal evidence never swaps the set /// and drops the keys Google still signs with. - function test_aReadingDatedTheSameSecondIsIgnored() public { + function test_aReadingDatedTheSameSecondIsRefused() public { uint64 at = _now(); _rotate(_readingAt(_oneKey("kid-1", "one"), at)); - vm.recordLogs(); - _rotate(_readingAt(_oneKey("kid-1", "two"), at)); // must not revert, must not apply + _refuses( + _readingAt(_oneKey("kid-1", "two"), at), abi.encodeWithSelector(GoogleJwtRoots.NotNewer.selector, at, at) + ); - assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); _assertGeneration(_current(), at, _one(_hash("one")), "current"); _assertEmpty(_previous(), "the same second shifted the list"); assertEq(roots.trustedHashExpiresAt(_hash("two")), 0, "the same second installed a key"); @@ -448,23 +446,55 @@ contract GoogleJwtRootsTest is Test { /// The rollback that matters: a GENUINELY signed older reading, replayed /// inside its freshness window after a newer one has landed. The signature - /// verifies; the reading's own clock is what refuses the regression -- - /// silently, so a keeper batch is never bricked. - function test_aGenuinelySignedOlderReadingIsIgnored() public { - bytes memory older = _readingAt(_oneKey("kid-1", "retired"), _now() - 30 minutes); + /// verifies; the reading's own clock is what refuses the regression, and + /// the error carries both clocks, so a keeper reading it knows which + /// reading it lost to. + function test_aGenuinelySignedOlderReadingIsRefused() public { + uint64 dated = _now() - 30 minutes; + bytes memory older = _readingAt(_oneKey("kid-1", "retired"), dated); uint64 at = _now(); bytes32 live = _install("kid-1", "live"); - vm.recordLogs(); - _rotate(older); // must not revert, must not apply + _refuses(older, abi.encodeWithSelector(GoogleJwtRoots.NotNewer.selector, dated, at)); - assertEq(vm.getRecordedLogs().length, 0, "a no-op emitted something"); _assertGeneration(_current(), at, _one(live), "older reading rolled the set back"); _assertEmpty(_previous(), "older reading shifted the list"); assertEq(roots.trustedHashExpiresAt(_hash("retired")), 0, "older reading installed a retired key"); assertEq(roots.trustedHashExpiresAt(live), at + roots.READING_LIFETIME(), "live key lost trust"); } + /// The front-run shape the silent return once existed for: a stranger + /// lands the keeper's own reading first. The keeper's transaction is + /// refused, its fee comes back with the revert -- gas is all it lost -- + /// and its next reading lands as usual. Nothing was consumed: with no + /// nullifier there is nothing a front-runner can spend on the keeper's + /// behalf, which is why "never revert, or the keeper is bricked" no + /// longer applies. + function test_aFrontRunKeeperLosesGasOnly() public { + uint64 at = _now(); + bytes memory attested = _reading(_oneKey("kid-1", "one")); + bytes memory sig = _sign(attested); + address stranger = makeAddr("stranger"); + vm.deal(stranger, 1 ether); + vm.prank(stranger); + roots.rotate{value: FEE}(attested, sig); + + uint256 funds = keeper.balance; + vm.prank(keeper); + vm.expectRevert(abi.encodeWithSelector(GoogleJwtRoots.NotNewer.selector, at, at)); + roots.rotate{value: FEE}(attested, sig); + assertEq(keeper.balance, funds, "the refused reading kept the keeper's fee"); + assertEq(address(notary).balance, FEE, "one fee reached the service: the stranger's"); + + vm.warp(at + 1 minutes); + bytes memory next = _reading(_oneKey("kid-1", "one")); + sig = _sign(next); + vm.prank(keeper); + roots.rotate{value: FEE}(next, sig); + assertEq(roots.freshestObservedAt(), at + 1 minutes, "the keeper's next reading landed"); + assertEq(address(notary).balance, 2 * FEE); + } + /// A newer reading of the set already current is Google saying the set /// still stands: its lifetime restarts from the new reading, nothing /// shifts, and the order Google listed the keys in is immaterial. @@ -562,14 +592,18 @@ contract GoogleJwtRootsTest is Test { } /// The freshest-reading mark is the current generation's clock: it only - /// ever moves forward, whether the newer reading refreshes or rotates. + /// ever moves forward, whether the newer reading refreshes or rotates, + /// and an older reading is refused against it. function test_freshestObservedAtIsTheReadingInForce() public { assertEq(roots.freshestObservedAt(), 0); uint64 first = _now(); _rotate(_readingAt(_oneKey("kid-1", "one"), first)); assertEq(roots.freshestObservedAt(), first); - _rotate(_readingAt(_oneKey("kid-1", "one"), first - 10 minutes)); + _refuses( + _readingAt(_oneKey("kid-1", "one"), first - 10 minutes), + abi.encodeWithSelector(GoogleJwtRoots.NotNewer.selector, first - 10 minutes, first) + ); assertEq(roots.freshestObservedAt(), first, "an older reading moved the mark"); vm.warp(first + 20 minutes); From 58df477f52d3bf7cf9c86e06aeb0ff362035bec5 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:01:22 +0100 Subject: [PATCH 11/15] test(ceremony): pin the storage words the two generations sit in test_upgrade_GoogleJwtRoots read both generations back through the ABI after the upgrade, which proves the values survived but not where they live. Read them by slot too: `notary` is word 0 of the namespace, a Generation is two words (its stamp, then its array's length word, with the elements off keccak256 of that slot), so `current` is words 1-2 and `previous` words 3-4. A field added to Generation moves `previous`, and an upgraded implementation would read the old generation's stamp as the new field -- that is the change this test now fails on, loudly, before any upgrade does. Checked by adding a uint256 to Generation: the word-2 pin fails first. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- .../ceremony/test/UpgradeSafety.t.sol | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index 1993a970..ca84656f 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -44,6 +44,7 @@ contract UpgradeSafetyTest is Test { bytes32 constant IMPL_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; bytes32 constant REENTRANCY_SLOT = 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; bytes32 constant NAMES_ROOT = 0x064503501234cc9c6e116cf4a84c07475158dabb6a3dcee437a89227e23bf200; + bytes32 constant ROOTS_ROOT = 0x7f78ff13201a03086d4b08e3085224c34a9fc247d0f67d11acd0db52976eb300; bytes32 constant X = HandleVectors.PLATFORM_X; function _slot(string memory ns) internal pure returns (bytes32) { @@ -66,9 +67,7 @@ contract UpgradeSafetyTest is Test { 0x92a7c997eeb454ceeeb8ef2d99ba7d4b830287ff966f129f9049c466a8342400 ); assertEq(_slot("libid.storage.IdentityNames"), NAMES_ROOT); - assertEq( - _slot("libid.storage.GoogleJwtRoots"), 0x7f78ff13201a03086d4b08e3085224c34a9fc247d0f67d11acd0db52976eb300 - ); + assertEq(_slot("libid.storage.GoogleJwtRoots"), ROOTS_ROOT); } function _implOf(address proxy) internal view returns (address) { @@ -282,6 +281,23 @@ contract UpgradeSafetyTest is Test { assertTrue(newer != older, "two different keys"); uint256 lifetime = r.READING_LIFETIME(); + // The layout those two generations sit in, pinned word by word. + // `notary` is word 0 of the namespace; a Generation is two words -- + // its stamp, then the length word of its dynamic array, whose + // elements hang off keccak256(that word's slot) -- so `current` is + // words 1-2 and `previous` words 3-4. A field added to Generation + // moves `previous`, and an upgraded implementation would then read + // the old generation's stamp as the new field: this is where that + // has to fail, loudly, before any upgrade does. + uint256 base = uint256(ROOTS_ROOT); + assertEq(uint256(vm.load(address(r), bytes32(base))), uint256(uint160(address(ns))), "word 0: notary"); + assertEq(uint256(vm.load(address(r), bytes32(base + 1))), second, "word 1: current.observedAt"); + assertEq(uint256(vm.load(address(r), bytes32(base + 2))), 1, "word 2: current.moduli.length"); + assertEq(vm.load(address(r), keccak256(abi.encode(base + 2))), newer, "current.moduli[0]"); + assertEq(uint256(vm.load(address(r), bytes32(base + 3))), first, "word 3: previous.observedAt"); + assertEq(uint256(vm.load(address(r), bytes32(base + 4))), 1, "word 4: previous.moduli.length"); + assertEq(vm.load(address(r), keccak256(abi.encode(base + 4))), older, "previous.moduli[0]"); + GoogleJwtRoots impl2 = new GoogleJwtRoots(); _expectNotOwner(); r.upgradeToAndCall(address(impl2), ""); From d129a71f7185da8a61f39de2807e3e5265b695da Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:41:56 +0100 Subject: [PATCH 12/15] docs(ceremony): the two-generation bound is a stated tradeoff The state-machine audit showed that a remove-before-add rotation served across Google's backends lets a submitter ordering genuine readings push a still-live key out until the next reading that lists it lands. That is liveness only and self-healing: nothing is inserted, tokens under the key fail meanwhile, and the keeper's next poll sees it untrusted and restores it. The user chose on 2026-09-03 to keep two generations over a per-modulus last-seen list, which would close the gap at the cost of remembering keys by modulus for a lifetime. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- solidity/contracts/ceremony/GoogleJwtRoots.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index e5d40d13..8722f831 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -36,6 +36,18 @@ import {INotaryService} from "./INotaryService.sol"; /// reading can land in between. One generation back covers that; /// further back is history. /// +/// Two generations remember two distinct sets, and that bound is a +/// known tradeoff. Google's backends do not all answer alike during a +/// rotation, so if three distinct sets are ever served at once and two +/// of them lack the same still-live key -- a remove-before-add +/// rotation -- a submitter ordering genuine readings can push that key +/// out until the next reading that lists it lands. Tokens under it fail +/// meanwhile; nothing is inserted; the keeper's next poll sees the key +/// untrusted and restores it. Accepted (2026-09-03) over a per-modulus +/// last-seen list, which would close it at the cost of remembering keys +/// by modulus for a lifetime; revisit if Google's rotation ever shows +/// that shape. +/// /// The reading is an ordinary notarized session -- the section 9.1 /// record every Platform Verifier consumes, authenticated by the same /// Notary Service and charged the same Notary Fee. What differs is the From 3faa41fc9edff319abe779fce5c0f65ff1538c37 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:47:09 +0100 Subject: [PATCH 13/15] docs: drop the last mentions of the products this branch removed The handle-vector paragraph said the Rust and TypeScript outputs would activate once those packages existed; they exist and CI checks them. The package README documented bindCall and encodeGitHubProof, which left with the hand-written constructions; the claim builder is generated now. The deploy script pointed a 'rotation listener' at the roots; that is the keeper. The roots contract said a second provider would want the same list; it would want one of its own, pinned to its own host. Factory tests named libid.registry and libid.bank, contracts that no longer exist. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- README.md | 6 +++-- rust/contracts/tests/anvil.rs | 14 +++++++---- .../contracts/ceremony/GoogleJwtRoots.sol | 3 ++- .../contracts/factory/test/LibidFactory.t.sol | 6 ++--- solidity/script/Deploy.s.sol | 2 +- ts/packages/contracts/README.md | 25 ++++++++++++------- ts/packages/contracts/package.json | 2 +- 7 files changed, 36 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5bebc42c..71440084 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,10 @@ python3 scripts/regen-identity-handles.py # rewrite generated outputs python3 scripts/regen-identity-handles.py --check # verify nothing drifted ``` -Today this generates `solidity/contracts/identity/HandleVectors.sol`; the Rust -and TypeScript outputs activate once those packages exist in this repo. +This generates `solidity/contracts/identity/HandleVectors.sol`, +`rust/identity/src/handle_vectors.rs` and +`ts/packages/contracts/src/identity/handleVectors.ts`; CI's handle-tables job +fails when any of them drifts from `handles.json`. ## Releasing diff --git a/rust/contracts/tests/anvil.rs b/rust/contracts/tests/anvil.rs index 244e4af8..5a816a30 100644 --- a/rust/contracts/tests/anvil.rs +++ b/rust/contracts/tests/anvil.rs @@ -368,11 +368,15 @@ async fn bootstraps_the_deterministic_factory_and_deploys_through_it() { creation_code .extend_from_slice(&(notary_impl, Bytes::from(init_data)).abi_encode_params()); - let predicted = predict_address(factory, "libid.notary"); - let deployed = - factory_deploy(&provider, factory, "libid.notary", creation_code.into()) - .await - .unwrap(); + let predicted = predict_address(factory, "libid.NotaryService"); + let deployed = factory_deploy( + &provider, + factory, + "libid.NotaryService", + creation_code.into(), + ) + .await + .unwrap(); assert_eq!(deployed, predicted); let notary = NotaryService::new(deployed, &provider); diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index 8722f831..960a9b83 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -76,7 +76,8 @@ import {INotaryService} from "./INotaryService.sol"; /// /// **Why a contract of its own.** A verifier should verify. Rotation is a /// separate concern with a separate caller set and a separate key list, -/// and a second OIDC provider would want the same list. +/// and a second OIDC provider would want a list of its own, of this +/// shape, pinned to its own host. /// /// **The trust model, in one line: the notary attests the reading, time /// does the expiry, and nobody owns rotation.** Verifiers check the stamp diff --git a/solidity/contracts/factory/test/LibidFactory.t.sol b/solidity/contracts/factory/test/LibidFactory.t.sol index d822f193..3fb17077 100644 --- a/solidity/contracts/factory/test/LibidFactory.t.sol +++ b/solidity/contracts/factory/test/LibidFactory.t.sol @@ -96,7 +96,7 @@ contract LibidFactoryTest is Test { } function test_predict_matchesActualForSeveralNames() public { - string[3] memory names = ["libid.notary", "libid.registry", "libid.bank"]; + string[3] memory names = ["libid.NotaryService", "libid.IdentityNames", "libid.GoogleJwtRoots"]; for (uint256 i = 0; i < names.length; i++) { address predicted = factory.predict(names[i]); vm.prank(owner); @@ -125,9 +125,9 @@ contract LibidFactoryTest is Test { abi.encode(address(notaryImpl), abi.encodeCall(NotaryService.initialize, (owner, signer, 7))) ); - address predicted = factory.predict("libid.notary"); + address predicted = factory.predict("libid.NotaryService"); vm.prank(owner); - address deployed = factory.deploy("libid.notary", creationCode); + address deployed = factory.deploy("libid.NotaryService", creationCode); assertEq(deployed, predicted); NotaryService notary = NotaryService(deployed); diff --git a/solidity/script/Deploy.s.sol b/solidity/script/Deploy.s.sol index 2d470473..03c63744 100644 --- a/solidity/script/Deploy.s.sol +++ b/solidity/script/Deploy.s.sol @@ -111,7 +111,7 @@ contract Deploy is Script { // Nothing is trusted until a notarized reading of Google's JWKS lands. // Until then every Google claim reverts `UntrustedModulus`, which reads // as a bad proof rather than an unseeded list. - console.log("NOTE: point a JWKS rotation listener at GOOGLE_JWT_ROOTS_ADDRESS"); + console.log("NOTE: point the keeper at GOOGLE_JWT_ROOTS_ADDRESS"); console.log(" before Google names work. The trust list starts empty."); } diff --git a/ts/packages/contracts/README.md b/ts/packages/contracts/README.md index be736525..59a794fd 100644 --- a/ts/packages/contracts/README.md +++ b/ts/packages/contracts/README.md @@ -1,8 +1,8 @@ # @libid/contracts Typed, viem-ready ABIs for every libid contract, a call builder for every -state-changing function, and the identity helper layer: handle normalization, -proof encoding for `bind`, and name resolution. +state-changing function, and the identity helper layer: handle normalization +and name resolution. Both `src/abis/` and `src/calls/` are generated from the forge artifacts (`solidity/out`) by `scripts/codegen.mjs`, and neither is committed — CI @@ -93,17 +93,24 @@ const { wallet, idAgrees } = await resolvePair(reader, x, 'alice', '42') const name = await primaryName(reader, wallet!, x) ``` -## Binding a name +## Claiming a name -`bindCall` builds calldata and nothing more — an EOA sends it directly, a -smart account wraps it in its own execute: +A claim is one of the generated builders: the platform, this chain's verifier +version for it, the opaque payload the ceremony produced, and whether to +publish the name. The value is what `quoteClaim` returns for the same pair. +An EOA sends it directly, a smart account wraps it in its own execute: ```ts -import { bindCall, encodeGitHubProof, platformId, PLATFORM_GITHUB_DOMAIN } from '@libid/contracts/identity' +import { calls } from '@libid/contracts/calls' -const proof = encodeGitHubProof(gitHubProof) // from the notarization flow -const call = bindCall(IDENTITY_NAMES, platformId(PLATFORM_GITHUB_DOMAIN), proof, true) -// call = { to, data } — sign and send from the address the proof names. +const fee = await client.readContract({ + address: IDENTITY_NAMES, + abi: identityNamesAbi, + functionName: 'quoteClaim', + args: [platformId(PLATFORM_GITHUB_DOMAIN), 1], +}) +const call = calls.identityNames.claim(IDENTITY_NAMES, fee, platformId(PLATFORM_GITHUB_DOMAIN), 1, payload, true) +// call = { to, value, data } — sign and send from the address the payload names. ``` ## Normalizing a handle locally diff --git a/ts/packages/contracts/package.json b/ts/packages/contracts/package.json index 156dfbaf..bc3a8d0d 100644 --- a/ts/packages/contracts/package.json +++ b/ts/packages/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@libid/contracts", "version": "0.6.0", - "description": "Typed viem-ready ABIs for the libid contracts, plus the identity helper layer (handle normalization, resolve, calldata).", + "description": "Typed viem-ready ABIs and call builders for the libid contracts, plus the identity helper layer (handle normalization, name resolution).", "license": "(MIT OR Apache-2.0)", "repository": { "type": "git", From dda202f70869f90520e498c0c72e5f746a2db707 Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:56:37 +0100 Subject: [PATCH 14/15] docs: the last stale words after the removals and the rename Comments and names that still described what this branch removed, or an older shape of what it kept. No behaviour changes. - IdentityNames.sol: `bind` was the legacy login entry point; the one write path is `claim`. Five references renamed, the "two paths" / "both entry points" rationales on CeremonyBound and _write rewritten for the single ceremony path, and the Platform storage comment no longer promises a "current version" the struct does not hold. - IdentityNames.t.sol: the non-normalizing-handle test is named after `claim`, the function it exercises. - CeremonyProfile.t.sol: the fixture opens with the X API authority id, not a format tag, platform id and session tag. - GitHubPlatformVerifier.t.sol: the mirrored X test sits at line 403. - UpgradeSafety.t.sol: the MainPlatform/PrPlatform layouts, the unused `_emulateMainPlatform` helper and the HandleNormalizer import that served only them are gone (per-platform `latestVersion` no longer exists); the stale-version-word test no longer speaks of "the PR". - ts codegen.mjs: the overload example named `webTransferV2`, a removed Bank function. - ts calls.anvil.test.ts: the removed OIDC proof-artifact tests are no longer cited as the skip precedent. - ts resolve.ts: names are claimed through a ceremony, and there is no wallet package to compare against. - ts package.json / tsconfig.build.json: drop the src/identity/fixtures exclusion, a directory that no longer exists. - regen-identity-handles.py: the Rust package exists and is a live, CI-checked output. - rust bindings: one stack, and one module per contract directory rather than "product areas". Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- rust/contracts/src/bindings/mod.rs | 2 +- rust/contracts/src/bindings/proxy.rs | 2 +- scripts/regen-identity-handles.py | 10 +++--- .../ceremony/test/CeremonyProfile.t.sol | 2 +- .../test/GitHubPlatformVerifier.t.sol | 2 +- .../ceremony/test/UpgradeSafety.t.sol | 33 ++----------------- solidity/contracts/identity/IdentityNames.sol | 25 +++++++------- .../identity/test/IdentityNames.t.sol | 4 +-- ts/packages/contracts/package.json | 3 +- ts/packages/contracts/scripts/codegen.mjs | 2 +- ts/packages/contracts/src/calls.anvil.test.ts | 4 +-- ts/packages/contracts/src/identity/resolve.ts | 8 ++--- ts/packages/contracts/tsconfig.build.json | 2 +- 13 files changed, 32 insertions(+), 67 deletions(-) diff --git a/rust/contracts/src/bindings/mod.rs b/rust/contracts/src/bindings/mod.rs index 0de60553..0de398fd 100644 --- a/rust/contracts/src/bindings/mod.rs +++ b/rust/contracts/src/bindings/mod.rs @@ -1,5 +1,5 @@ //! Hand-written `alloy::sol!` bindings, kept in lockstep with the Solidity -//! sources in `solidity/contracts`. Grouped by product area. +//! sources in `solidity/contracts`. One module per contract directory. pub mod ceremony; pub mod factory; diff --git a/rust/contracts/src/bindings/proxy.rs b/rust/contracts/src/bindings/proxy.rs index 47541372..e1d31f2c 100644 --- a/rust/contracts/src/bindings/proxy.rs +++ b/rust/contracts/src/bindings/proxy.rs @@ -1,4 +1,4 @@ -//! Proxy plumbing shared by every stack: the ERC1967 proxy constructor and +//! Proxy plumbing shared by every proxied contract: the ERC1967 proxy constructor and //! the UUPS upgrade entrypoint. #[allow(clippy::too_many_arguments, unused_attributes)] diff --git a/scripts/regen-identity-handles.py b/scripts/regen-identity-handles.py index dc6cf605..8629be95 100755 --- a/scripts/regen-identity-handles.py +++ b/scripts/regen-identity-handles.py @@ -8,8 +8,7 @@ rust/identity/src/handle_vectors.rs # Rust constants + vectors ts/packages/contracts/src/identity/handleVectors.ts # TypeScript constants + vectors -The Rust package does not exist in this repo yet; an output is skipped with a -note until its target directory appears. The normalizer +An output is skipped with a note until its target directory appears. The normalizer itself is hand written in each language. Only the constants and the vectors come from here. That is the point: several implementations, one vector table, so a difference between them fails a test instead of writing a different key @@ -34,8 +33,8 @@ SOL_OUT = SOLIDITY_ROOT / "contracts" / "identity" / "HandleVectors.sol" # An output is skipped (with a note) while its parent directory is absent, so -# `--check` holds only the outputs that exist to account. The TypeScript -# package is live; the Rust one is not in this repo yet. +# `--check` holds only the outputs that exist to account. Both the Rust and +# the TypeScript packages are live today. RUST_OUT = REPO_ROOT / "rust" / "identity" / "src" / "handle_vectors.rs" TS_OUT = REPO_ROOT / "ts" / "packages" / "contracts" / "src" / "identity" / "handleVectors.ts" @@ -486,8 +485,7 @@ def main() -> int: outputs = [] skipped = [] outputs.append((SOL_OUT, sol_formatted(gen_sol(spec)))) - # Emit an output only once its package directory exists. TS does; the Rust - # package is not in this repo yet. + # Emit an output only once its package directory exists (both do today). for path, gen in ((RUST_OUT, gen_rust), (TS_OUT, gen_ts)): if path.parent.exists(): outputs.append((path, gen(spec))) diff --git a/solidity/contracts/ceremony/test/CeremonyProfile.t.sol b/solidity/contracts/ceremony/test/CeremonyProfile.t.sol index 6952bbdf..01942d13 100644 --- a/solidity/contracts/ceremony/test/CeremonyProfile.t.sol +++ b/solidity/contracts/ceremony/test/CeremonyProfile.t.sol @@ -60,7 +60,7 @@ contract CeremonyProfileTest is Test { } /// @dev The pinned fixture in CeremonyAttestation.t.sol opens with the - /// format tag, the X platform id and the identity session tag. If + /// X API authority id and nothing else names a platform. If /// these constants and that fixture ever drift apart, one of them is /// wrong. function test_theAttestationFixtureUsesTheseConstants() public pure { diff --git a/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol b/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol index ed27bd00..ed0bfd41 100644 --- a/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol +++ b/solidity/contracts/ceremony/test/GitHubPlatformVerifier.t.sol @@ -395,7 +395,7 @@ contract GitHubPlatformVerifierTest is Test { this.run{value: quote}(s); } - /// REQ-PLAT-52B: mirror of XPlatformVerifier.t.sol:412 test_provesAgainstTheCommitmentsTheNotarySigned + /// REQ-PLAT-52B: mirror of XPlatformVerifier.t.sol:403 test_provesAgainstTheCommitmentsTheNotarySigned /// using this file's TOKEN_COMMITMENT / IDENTITY_COMMITMENT (verified passing as test_P25_…). function test_provesAgainstTheCommitmentsTheNotarySigned() public { /* copy of the X test */ } } diff --git a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol index ca84656f..51827754 100644 --- a/solidity/contracts/ceremony/test/UpgradeSafety.t.sol +++ b/solidity/contracts/ceremony/test/UpgradeSafety.t.sol @@ -18,7 +18,6 @@ import {IProofVerifier} from "../IProofVerifier.sol"; import {IdentityNames} from "../../identity/IdentityNames.sol"; import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; import {HandleVectors} from "../../identity/HandleVectors.sol"; -import {HandleNormalizer} from "../../identity/HandleNormalizer.sol"; import {IdentityNodes} from "../../identity/IdentityNodes.sol"; import {StubPlatformVerifier} from "../../identity/test/StubPlatformVerifier.sol"; import {AttestationBuilder} from "./AttestationBuilder.sol"; @@ -424,40 +423,12 @@ contract UpgradeSafetyTest is Test { assertEq(at, 1_780_000_000); } - // The two Platform layouts, replicated so the slot arithmetic is measured - // rather than asserted from memory. - struct MainPlatform { - HandleNormalizer.Rules rules; - uint32 latestVersion; - bool configured; - } - - struct PrPlatform { - HandleNormalizer.Rules rules; - bool configured; - } - - MainPlatform mainP; - PrPlatform prP; - - /// Emulate the storage a main-deployed IdentityNames proxy holds for a - /// configured platform, then read it through the PR implementation. - function _emulateMainPlatform(uint32 latestVersion, bool configured) internal { - bytes32 entry = keccak256(abi.encode(X, uint256(NAMES_ROOT) + 3)); - HandleNormalizer.Rules memory r = HandleVectors.rulesFor(X); - uint256 rulesWord = uint256(r.maxLength) | (r.stripLeadingAt ? 1 << 16 : 0) | (r.isEmail ? 1 << 24 : 0) - | (r.allowUnderscore ? 1 << 32 : 0) | (r.allowHyphen ? 1 << 40 : 0); - vm.store(address(names), entry, bytes32(rulesWord)); - uint256 w = uint256(latestVersion) | (configured ? uint256(1) << 32 : 0); - vm.store(address(names), bytes32(uint256(entry) + 1), bytes32(w)); - } - function _claimExternal(address who, uint256 nonce) external { _claimAs(who, nonce); } - /// Binding lost `version` (uint32 at byte offset 28). Stale bits in that - /// word are ignored by the PR's reads, and a fresh write leaves them as-is. + /// `Binding` once carried a `version` (uint32 at byte offset 28). Stale bits + /// in that word are ignored by the current reads, and a fresh write leaves them as-is. function test_bindingStaleVersionWordIsIgnored() public { _names(); vm.prank(OWNER); diff --git a/solidity/contracts/identity/IdentityNames.sol b/solidity/contracts/identity/IdentityNames.sol index 616c956d..871c3197 100644 --- a/solidity/contracts/identity/IdentityNames.sol +++ b/solidity/contracts/identity/IdentityNames.sol @@ -25,7 +25,7 @@ import {IdentityNodes} from "./IdentityNodes.sol"; /// to, and that address has to be the caller. Nothing else grants a /// binding: there is no owner function that writes, moves or deletes an /// entry in either mapping. An account's own fresh proof retires the -/// handle that account used to hold, and nobody else's — see `bind`. +/// handle that account used to hold, and nobody else's — see `claim`. /// /// **What the owner can still do, stated plainly.** It configures which /// verifiers a platform uses, and a verifier is trusted to report what a @@ -101,7 +101,7 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl /// **The account id follows from what a version is.** A version is /// another way to prove the SAME account — a notarized session, an /// OIDC token — not another account space. The account did not change, - /// so its id did not change, and `bind` keys every version on the same + /// so its id did not change, and `claim` keys every version on the same /// `idNode(platformId, attested.userId)`. /// /// Read that as a test, not as a rule to remember: a proof format that @@ -157,7 +157,7 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl /// the plaintext either way, so an indexer never needs this, and only a /// contract that must display a name does. mapping(address => mapping(bytes32 => string)) published; - /// platformId -> its keyspace: handle rules and current version. + /// platformId -> its keyspace: handle rules, and whether it is configured. mapping(bytes32 => Platform) platforms; /// idNode -> the handle node that account last proved, and back. /// @@ -267,10 +267,9 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl /// @notice What a ceremony carried that a binding does not keep. /// - /// @dev Beside `IdentityBound`, not inside it. The two paths share that - /// event, and only a ceremony authenticates an OAuth client -- a field - /// the legacy path could never fill is a field an indexer must always - /// test for emptiness. + /// @dev Beside `IdentityBound`, not inside it: the binding keeps no + /// client identifier, so carrying one on `IdentityBound` would give + /// every indexer a field to test for emptiness. /// /// `clientIdentifier` is the exact bytes the platform authenticated. /// The contract has no use for them: the digest already binds the @@ -285,7 +284,7 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl /// @notice A handle stopped resolving because the account that held it /// proved a different one. - /// @dev Nobody else's entry can be retired this way. See `bind`. + /// @dev Nobody else's entry can be retired this way. See `claim`. event HandleRetired(bytes32 indexed platformId, bytes32 indexed handleNode, address indexed owner); /// @notice A platform's keyspace was configured or reconfigured. @@ -494,11 +493,9 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl emit ProofVerifierConfigured(address(verifier)); } - /// @dev Everything after authentication, shared by both entry points. - /// - /// Which proof established a name is the authentication half's - /// business; the keyspace, the ordering and the display are the same - /// whichever half ran. + /// @dev Everything after authentication. Which proof established a name + /// is `claim`'s business; the keyspace, the ordering and the display + /// are decided here. function _write( bytes32 platformId, string memory userId, @@ -590,7 +587,7 @@ contract IdentityNames is Initializable, UUPSUpgradeable, Ownable2StepUpgradeabl /// @notice Withdraw a published handle. Affects the caller's record only. /// /// @dev Publishing is the one thing here a user can undo, and it needs its - /// own door. Passing `publishName: false` to `bind` does NOT clear an + /// own door. Passing `publishName: false` to `claim` does NOT clear an /// earlier publish — a caller that binds again after a rename should /// not silently withdraw a name because a flag defaulted; it refreshes /// the published string to the handle just proved. Withdrawing what diff --git a/solidity/contracts/identity/test/IdentityNames.t.sol b/solidity/contracts/identity/test/IdentityNames.t.sol index 9fd9681b..0b5e112e 100644 --- a/solidity/contracts/identity/test/IdentityNames.t.sol +++ b/solidity/contracts/identity/test/IdentityNames.t.sol @@ -490,9 +490,9 @@ contract IdentityNamesTest is Test { names.resolveHandle(unknown, "alice"); } - /// `bind` keeps reverting. A handle that arrives inside a proof and does + /// `claim` keeps reverting. A handle that arrives inside a proof and does /// not normalize is a broken proof, and failing loudly is right. - function test_bindStillRefusesAHandleThatDoesNotNormalize() public { + function test_claimStillRefusesAHandleThatDoesNotNormalize() public { _stage("123", "ali ce", alice, 100); vm.prank(alice); vm.expectRevert(HandleNormalizer.BadCharacter.selector); diff --git a/ts/packages/contracts/package.json b/ts/packages/contracts/package.json index bc3a8d0d..6050a785 100644 --- a/ts/packages/contracts/package.json +++ b/ts/packages/contracts/package.json @@ -15,8 +15,7 @@ "files": [ "dist", "src", - "!src/**/*.test.ts", - "!src/identity/fixtures" + "!src/**/*.test.ts" ], "exports": { ".": { diff --git a/ts/packages/contracts/scripts/codegen.mjs b/ts/packages/contracts/scripts/codegen.mjs index b1eff933..ca1cff40 100644 --- a/ts/packages/contracts/scripts/codegen.mjs +++ b/ts/packages/contracts/scripts/codegen.mjs @@ -104,7 +104,7 @@ export const ${exportName} = ${body} as const satisfies Abi /// Arguments are not spelled out here: `ContractFunctionArgs` reads them off /// the ABI, so there is no Solidity-to-TypeScript type table in this script to /// drift from the real one. It resolves overloads to a union of tuples too, -/// which is what lets `webTransferV2` keep one name. +/// which is what keeps one builder per name should a contract ever overload one. /// /// Payable functions take `value` before their arguments. That is the whole /// reason to distinguish them: a caller who forgets the value on a payable diff --git a/ts/packages/contracts/src/calls.anvil.test.ts b/ts/packages/contracts/src/calls.anvil.test.ts index c8c2b0e7..61dfbc87 100644 --- a/ts/packages/contracts/src/calls.anvil.test.ts +++ b/ts/packages/contracts/src/calls.anvil.test.ts @@ -6,8 +6,8 @@ /// whose arguments have to land in the right order — then reads the balances /// back. Without it, "generated" and "usable" are two different claims. /// -/// Skips when anvil or the compiled artifact is missing, like the OIDC -/// proof-artifact tests do. CI always has both: the same job installs foundry +/// Skips when anvil or the compiled artifact is missing. CI always has both: +/// the same job installs foundry /// and builds the contracts this reads. import { spawn, spawnSync } from 'node:child_process' import { readFileSync } from 'node:fs' diff --git a/ts/packages/contracts/src/identity/resolve.ts b/ts/packages/contracts/src/identity/resolve.ts index fee56b8b..3a6b988e 100644 --- a/ts/packages/contracts/src/identity/resolve.ts +++ b/ts/packages/contracts/src/identity/resolve.ts @@ -1,8 +1,8 @@ /// Reading names from the chain. /// -/// Everything here is a view call. Binding a name needs a proof and a wallet; -/// reading one needs neither, which is the point — any product can resolve a -/// name without touching the login that created it. +/// Everything here is a view call. Claiming a name needs a ceremony payload and a +/// wallet; reading one needs neither, which is the point — any product can +/// resolve a name without touching the ceremony that created it. /// /// The client and the contract address are arguments rather than imports. This /// package ships separately from the repository it currently lives in, so it @@ -26,7 +26,7 @@ export interface NamesReader { /// One view call. /// -/// Routed through a narrow cast for the same reason the wallet package does it: +/// Routed through a narrow cast for one reason: /// viem's `PublicClient`, without its chain type parameters, types /// `authorizationList` as required even for a read. The alternative is to /// spread that noise across every call site here. diff --git a/ts/packages/contracts/tsconfig.build.json b/ts/packages/contracts/tsconfig.build.json index ad49fb21..0a11719f 100644 --- a/ts/packages/contracts/tsconfig.build.json +++ b/ts/packages/contracts/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["src/**/*.test.ts", "src/identity/fixtures"] + "exclude": ["src/**/*.test.ts"] } From f330f8613c527eee4ca28cad86039117c0efdbef Mon Sep 17 00:00:00 2001 From: xgreenx Date: Thu, 3 Sep 2026 13:58:22 +0100 Subject: [PATCH 15/15] docs(ceremony): say what the code does, not what it replaced The NatSpec of the newer-reading rule and two test comments narrated the silent return that an earlier draft of this branch had, as a changelog inside the contract. The commit that made the change carries that story; the code says what holds now. The real-session test cited the capture tool by a path that exists in the notary repository, not this one, and the package README claimed ABIs for every libid contract where it ships the identity stack's. Assisted-by: Claude Fable 5.1 Signed-off-by: xgreenx --- solidity/contracts/ceremony/GoogleJwtRoots.sol | 15 +++++---------- .../contracts/ceremony/test/GoogleJwtRoots.t.sol | 14 ++++++-------- .../ceremony/test/GoogleJwtRootsRealSession.t.sol | 2 +- ts/packages/contracts/README.md | 7 ++++--- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/solidity/contracts/ceremony/GoogleJwtRoots.sol b/solidity/contracts/ceremony/GoogleJwtRoots.sol index 960a9b83..2ee9c539 100644 --- a/solidity/contracts/ceremony/GoogleJwtRoots.sol +++ b/solidity/contracts/ceremony/GoogleJwtRoots.sol @@ -698,16 +698,11 @@ contract GoogleJwtRoots is Initializable, UUPSUpgradeable, Ownable2StepUpgradeab /// `NotNewer`. Rotation is open and a reading binds no contract, so /// anyone can replay any still-fresh reading anywhere, forever; the /// refusal keeps an older reading from rolling the set back and a - /// replay from re-stamping the lifetime. It used to be a silent - /// return, on the rule that a permissionless path must never revert - /// or a front-runner could brick the honest keeper by landing first. - /// That rule belonged to the per-claim nullifier design, where the - /// first submission consumed the claim; here nothing is consumed -- - /// the front-runner's reading IS the keeper's, applied -- so the - /// keeper's transaction has nothing left to do, and a revert says so - /// while rolling the Notary Fee back with the state. The return - /// charged the fee for nothing done and gave the keeper no signal. - /// No later includes equal: a second reading dated the same second + /// replay from re-stamping the lifetime. A front-runner landing the + /// keeper's own reading first consumes nothing -- that reading IS + /// the keeper's, applied -- so the keeper's transaction has nothing + /// left to do, and the revert says so while rolling the Notary Fee + /// back with the state. No later includes equal: a second reading dated the same second /// cannot swap the set -- the rule for equal evidence everywhere in /// the protocol (REQ-COMMON-25A). The freshness checks in `rotate` /// run first, so a stale or future record is named as such. diff --git a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol index 6ae1406b..f975400b 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRoots.t.sol @@ -411,7 +411,7 @@ contract GoogleJwtRootsTest is Test { /// reading's own clock on both sides. Nothing is written -- in particular /// the lifetime does NOT restart, so spamming one reading for its whole /// freshness window cannot stretch trust by a second -- and the revert - /// hands the fee back, where a silent return charged it for nothing done. + /// hands the fee back rather than charging it for nothing done. function test_replayingTheReadingInForceIsRefused() public { uint64 at = _now(); bytes memory attested = _reading(_oneKey("kid-1", "one")); @@ -463,13 +463,11 @@ contract GoogleJwtRootsTest is Test { assertEq(roots.trustedHashExpiresAt(live), at + roots.READING_LIFETIME(), "live key lost trust"); } - /// The front-run shape the silent return once existed for: a stranger - /// lands the keeper's own reading first. The keeper's transaction is - /// refused, its fee comes back with the revert -- gas is all it lost -- - /// and its next reading lands as usual. Nothing was consumed: with no - /// nullifier there is nothing a front-runner can spend on the keeper's - /// behalf, which is why "never revert, or the keeper is bricked" no - /// longer applies. + /// The front-run shape: a stranger lands the keeper's own reading first. + /// The keeper's transaction is refused, its fee comes back with the + /// revert -- gas is all it lost -- and its next reading lands as usual. + /// Nothing was consumed: with no nullifier there is nothing a + /// front-runner can spend on the keeper's behalf. function test_aFrontRunKeeperLosesGasOnly() public { uint64 at = _now(); bytes memory attested = _reading(_oneKey("kid-1", "one")); diff --git a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol index 08b9e272..38ec7ddc 100644 --- a/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol +++ b/solidity/contracts/ceremony/test/GoogleJwtRootsRealSession.t.sol @@ -18,7 +18,7 @@ import {GoogleJwtRoots} from "../GoogleJwtRoots.sol"; /// response is framed by the test, and the notary is `vm.sign`. Every byte /// there is one a test author chose. This file starts from the other end: the /// fixture is a genuine MPC-TLS session with www.googleapis.com, captured on -/// 2026-09-03 by `notary/examples/notarize_jwks.rs` through a notary signing +/// 2026-09-03 by libid-org/notary's `examples/notarize_jwks.rs` through a notary signing /// with anvil #0, and nothing in it was written by hand -- the request line is /// what the keeper's prover put on the wire, the head is what Google's front /// end answered, the framing is Google's, the key set is the one Google diff --git a/ts/packages/contracts/README.md b/ts/packages/contracts/README.md index 59a794fd..aec3a923 100644 --- a/ts/packages/contracts/README.md +++ b/ts/packages/contracts/README.md @@ -1,8 +1,9 @@ # @libid/contracts -Typed, viem-ready ABIs for every libid contract, a call builder for every -state-changing function, and the identity helper layer: handle normalization -and name resolution. +Typed, viem-ready ABIs for the libid identity stack (NotaryService, +CeremonyProofVerifier, GoogleJwtRoots, IdentityNames, LibidFactory, WTIA9), a +call builder for every state-changing function, and the identity helper layer: +handle normalization and name resolution. Both `src/abis/` and `src/calls/` are generated from the forge artifacts (`solidity/out`) by `scripts/codegen.mjs`, and neither is committed — CI