Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
75 changes: 75 additions & 0 deletions script/AddRedditGroups.s.sol
Original file line number Diff line number Diff line change
@@ -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=<key> CREDENTIAL_REGISTRY_ADDRESS=0x17a22f130d4e1c4ba5C20a679a5a29F227083A62 \
/// forge script script/AddRedditGroups.s.sol:AddRedditGroups \
/// --rpc-url <rpc> --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);
}
}
}
25 changes: 21 additions & 4 deletions script/CredentialGroups.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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=<key> CREDENTIAL_REGISTRY_ADDRESS=<addr> \
Expand All @@ -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;
Expand Down Expand Up @@ -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]);
Expand Down