diff --git a/CLAUDE.md b/CLAUDE.md index 534af79..76262b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,9 @@ identity = new Identity(seed) | 13 | Apple Subs | — | 0 | 10 | 180 days | | 14 | Binance KYC | — | 0 | 20 | 180 days | | 15 | OKX KYC | — | 0 | 20 | 180 days | +| 16 | Reddit | Low | 4 | 2 | 30 days | +| 17 | Reddit | Medium | 4 | 5 | 60 days | +| 18 | Reddit | High | 4 | 10 | 90 days | ## Architecture @@ -147,7 +150,7 @@ contracts/ - **Ownable2Step** (OpenZeppelin) for admin operations — two-step ownership transfer. - **Per-app Semaphore groups**: each (credentialGroup, app) pair gets its own Semaphore group, created lazily on first registration. Since Semaphore enforces per-group nullifier uniqueness, separate groups per app naturally prevent cross-app proof replay — no second circuit needed. - **Credential state**: per-credential state is stored in a single `credentials` mapping (`bytes32 registrationHash => CredentialRecord`). The registration hash uses a two-slot encoding to prevent collisions: for family groups (familyId > 0): `keccak256(registry, familyId, 0, credentialId, appId)` — all groups in the same family share one slot; for standalone groups (familyId == 0): `keccak256(registry, 0, credentialGroupId, credentialId, appId)`. The `credentialGroupId` is stored in `CredentialRecord` to track which specific group the credential belongs to. -- **Family enforcement**: credential groups with the same `familyId` (> 0) share a registration hash, so a user can only hold one credential per family per app (e.g. cannot have both Farcaster Low and Farcaster High). Group changes within a family go through the recovery timelock (`initiateRecovery`/`executeRecovery`) to prevent double-spend with different Semaphore nullifiers. Standalone groups (familyId = 0) have no family constraint. Family IDs: 1 = Farcaster (groups 1–3), 2 = GitHub (groups 4–6), 3 = X/Twitter (groups 7–9), 0 = standalone (groups 10–15). +- **Family enforcement**: credential groups with the same `familyId` (> 0) share a registration hash, so a user can only hold one credential per family per app (e.g. cannot have both Farcaster Low and Farcaster High). Group changes within a family go through the recovery timelock (`initiateRecovery`/`executeRecovery`) to prevent double-spend with different Semaphore nullifiers. Standalone groups (familyId = 0) have no family constraint. Family IDs: 1 = Farcaster (groups 1–3), 2 = GitHub (groups 4–6), 3 = X/Twitter (groups 7–9), 4 = Reddit (groups 16–18), 0 = standalone (groups 10–15). - **Scope binding**: `submitProof` ties proofs to `appId_` + `msg.sender` + a context value via `scope == keccak256(abi.encode(appId_, msg.sender, context))`, preventing proof replay across apps and callers. The `appId_` is a consumer-controlled function parameter (not taken from the proof struct), so an attacker cannot substitute a different app's scorer. - **App-specific identities**: each app derives a unique Semaphore commitment from `keccak256(abi.encodePacked(walletPrivateKey, appId, credentialGroupId))` fed into `new Identity(seed)`. This ensures per-app and per-credential-group isolation. - **Trusted verifiers**: multiple signers supported via `trustedVerifiers` mapping with `addTrustedVerifier`/`removeTrustedVerifier`. Supports TLSN, OAuth, zkPassport, etc. diff --git a/script/AddRedditGroups.s.sol b/script/AddRedditGroups.s.sol new file mode 100644 index 0000000..b1b24bc --- /dev/null +++ b/script/AddRedditGroups.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {CredentialRegistry} from "../contracts/registry/CredentialRegistry.sol"; +import {ICredentialRegistry} from "@bringid/contracts/interfaces/ICredentialRegistry.sol"; +import {DefaultScorer} from "@bringid/contracts/scoring/DefaultScorer.sol"; +import {Script, console} from "forge-std/Script.sol"; + +/// @notice Adds Reddit (zkTLS) credential groups to an existing CredentialRegistry. +/// +/// ID | Credential | Group | Family | Duration | Score +/// ---|------------|--------|--------|----------|------ +/// 16 | Reddit | Low | 4 | 30 days | 2 +/// 17 | Reddit | Medium | 4 | 60 days | 5 +/// 18 | Reddit | High | 4 | 90 days | 10 +/// +/// Usage: +/// PRIVATE_KEY= CREDENTIAL_REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 \ +/// forge script script/AddRedditGroups.s.sol:AddRedditGroups \ +/// --rpc-url --broadcast +contract AddRedditGroups is Script { + function run() public { + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + + CredentialRegistry registry = CredentialRegistry(vm.envAddress("CREDENTIAL_REGISTRY_ADDRESS")); + DefaultScorer scorer = DefaultScorer(registry.defaultScorer()); + + // --- Reddit credential groups (family 4) --- + uint256[] memory ids = new uint256[](3); + uint256[] memory durations = new uint256[](3); + uint256[] memory families = new uint256[](3); + uint256[] memory scores = new uint256[](3); + + // Reddit Low — 30 days, score 2 + ids[0] = 16; + durations[0] = 30 days; + families[0] = 4; + scores[0] = 2; + + // Reddit Medium — 60 days, score 5 + ids[1] = 17; + durations[1] = 60 days; + families[1] = 4; + scores[1] = 5; + + // Reddit High — 90 days, score 10 + ids[2] = 18; + durations[2] = 90 days; + families[2] = 4; + scores[2] = 10; + + // Create credential groups that don't already exist + for (uint256 i = 0; i < ids.length; i++) { + (ICredentialRegistry.CredentialGroupStatus status,,) = registry.credentialGroups(ids[i]); + if (status == ICredentialRegistry.CredentialGroupStatus.UNDEFINED) { + registry.createCredentialGroup(ids[i], durations[i], families[i]); + console.log("Created group %d", ids[i]); + } else { + console.log("Group %d already exists, skipping", ids[i]); + } + } + + // Set scores on the DefaultScorer + scorer.setScores(ids, scores); + + vm.stopBroadcast(); + + // --- verification logging --- + for (uint256 i = 0; i < ids.length; i++) { + (ICredentialRegistry.CredentialGroupStatus status,,) = registry.credentialGroups(ids[i]); + uint256 score = scorer.getScore(ids[i]); + console.log("Group %d: status=%d, score=%d", ids[i], uint256(status), score); + } + } +} diff --git a/script/CredentialGroups.s.sol b/script/CredentialGroups.s.sol index cd4adaa..324acef 100644 --- a/script/CredentialGroups.s.sol +++ b/script/CredentialGroups.s.sol @@ -26,6 +26,9 @@ import {Script, console} from "forge-std/Script.sol"; /// 13 | Apple Subs | — | 10 /// 14 | Binance KYC | — | 20 /// 15 | OKX KYC | — | 20 +/// 16 | Reddit | Low | 2 +/// 17 | Reddit | Medium | 5 +/// 18 | Reddit | High | 10 /// /// Usage: /// PRIVATE_KEY= CREDENTIAL_REGISTRY_ADDRESS= \ @@ -39,10 +42,10 @@ contract DeployCredentialGroups is Script { DefaultScorer scorer = DefaultScorer(registry.defaultScorer()); // --- credential group IDs, validity durations, families, and scores --- - uint256[] memory ids = new uint256[](15); - uint256[] memory durations = new uint256[](15); - uint256[] memory families = new uint256[](15); - uint256[] memory scores = new uint256[](15); + uint256[] memory ids = new uint256[](18); + uint256[] memory durations = new uint256[](18); + uint256[] memory families = new uint256[](18); + uint256[] memory scores = new uint256[](18); // Farcaster Low / Medium / High — 30 / 60 / 90 days, family 1 ids[0] = 1; @@ -110,6 +113,20 @@ contract DeployCredentialGroups is Script { durations[14] = 180 days; scores[14] = 20; + // Reddit Low / Medium / High — 30 / 60 / 90 days, family 4 + ids[15] = 16; + durations[15] = 30 days; + families[15] = 4; + scores[15] = 2; + ids[16] = 17; + durations[16] = 60 days; + families[16] = 4; + scores[16] = 5; + ids[17] = 18; + durations[17] = 90 days; + families[17] = 4; + scores[17] = 10; + // Create credential groups that don't already exist for (uint256 i = 0; i < ids.length; i++) { (ICredentialRegistry.CredentialGroupStatus status,,) = registry.credentialGroups(ids[i]);