Architecture & How It Works
Self Connect: Architecture & How It Works
Protocol Architecture
Self Connect uses a federated architecture where multiple independent issuers can register attestations that map obfuscated identifiers to blockchain addresses.
Core Components
┌─────────────────┐
│ User/Client │
└────────┬────────┘
│
├──────────────┐
│ │
v v
┌─────────────┐ ┌─────────────┐
│ ODIS │ │ Issuer │
│ (Privacy) │ │(Verification)│
└──────┬──────┘ └──────┬──────┘
│ │
└────────┬───────┘
│
v
┌──────────────────────┐
│ FederatedAttestations│
│ Smart Contract │
└──────────────────────┘
FederatedAttestations Smart Contract
The FederatedAttestations contract is the on-chain registry for all attestation mappings. It stores attestations in a structure that allows:
- Registration: Issuers register mappings between obfuscated identifiers and addresses
- Lookup: Anyone can query attestations for a given obfuscated identifier
- Multi-Issuer Support: Attestations are organized by issuer, allowing trust-based filtering
- Batch Operations: Multiple attestations can be queried simultaneously
Key Functions:
// Register an attestation as an issuer
function registerAttestationAsIssuer(
bytes32 identifier,
address account,
uint64 issuedOn
) external
// Lookup attestations for an identifier across multiple issuers
function lookupAttestations(
bytes32 identifier,
address[] calldata trustedIssuers
) external view returns (
uint256[] memory countsPerIssuer,
address[] memory accounts,
address[] memory signers,
uint64[] memory issuedOns,
uint64[] memory publishedOns
)
Contract Address:
- Mainnet: View on Celoscan
- Alfajores Testnet: View on Celoscan
Issuer Ecosystem
Issuers are independent entities that:
- Verify user ownership of identifiers through their chosen method
- Request obfuscated identifiers from ODIS
- Register attestations to the FederatedAttestations contract
- Maintain ODIS quota for continued operations
Applications choose which issuers to trust based on:
- Verification rigor
- Reputation
- Use case alignment
- Uptime and reliability
ODIS (Oblivious Decentralized Identifier Service)
ODIS is the privacy-preserving component of Self Connect that ensures identifiers cannot be reverse-engineered from on-chain data.
What is ODIS
ODIS implements a rate-limited Oblivious Pseudorandom Function (OPRF) that allows users to compute a limited number of hashes without letting the service see the data being hashed.
Key Properties:
- Oblivious: ODIS operators cannot see the plaintext identifiers
- Decentralized: No single party can compute the pepper alone
- Rate-Limited: Prevents rainbow table attacks through quota system
- Deterministic: Same input always produces same output
Privacy Guarantees
ODIS provides strong privacy guarantees through its design:
1. Blinding Process
When a client queries ODIS:
1. Client blinds the identifier locally using a secret one-time key
2. Blinded value is sent to ODIS operators
3. Operators compute the OPRF on the hidden input
4. Operators return the blinded result
5. Client unblinds the result to get the final pepper
This process ensures that:
- ODIS operators never see the plaintext identifier
- Even if all operators are compromised, user privacy is maintained
- No targeted censorship is possible
2. Threshold Cryptography
ODIS uses a (k, m) threshold signature scheme:
- m: Total number of ODIS operators
- k: Minimum number of signatures required
Security Properties:
- If fewer than k operators are compromised: Attackers cannot compute unauthorized peppers
- If at least k operators are honest: Service remains available for legitimate users
Example Configuration:
- 7 operators (m=7)
- 5 required signatures (k=5)
- Security: Need 5 compromised operators to break privacy
- Availability: Can tolerate 2 operators being offline
3. Distributed Key Generation (DKG)
Before deployment, ODIS operators participated in a DKG ceremony to generate a shared secret, split across all operators. Each operator holds a key share that can be used to sign responses.
When enough signatures (≥k) are combined, they produce the unique, deterministic pepper for that identifier.
Quota System and Rate Limiting
ODIS implements a quota system to prevent rainbow table attacks while allowing legitimate usage.
Quota Factors
Quota is based on:
- Payment: Pay for quota using cUSD
- 10 cUSD = 10,000 queries
- ~0.001 cUSD per query
- Account-based Limits: Rate limits per account to prevent abuse
How Quota Works
// Check current quota
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
issuerAddress,
authSigner,
serviceContext
);
// Purchase quota if needed
if (remainingQuota < 1) {
const stableTokenContract = await kit.contracts.getStableToken();
const odisPaymentsContract = await kit.contracts.getOdisPayments();
const ONE_CENT_CUSD_WEI = 10000000000000000;
await stableTokenContract
.increaseAllowance(odisPaymentsContract.address, ONE_CENT_CUSD_WEI)
.sendAndWaitForReceipt();
await odisPaymentsContract
.payInCUSD(issuerAddress, ONE_CENT_CUSD_WEI)
.sendAndWaitForReceipt();
}
Rate Limiting Strategy
The quota system makes it prohibitively expensive to:
- Scrape large quantities of identifiers
- Build rainbow tables
- Perform mass surveillance
While still allowing:
- Normal user flows
- Legitimate application usage
- Reasonable issuer operations
Key Rotation
If an operator’s key is leaked or compromised, ODIS can perform key rotation:
- New DKG ceremony with at least k old keys participating
- New keys generated for all operators (including new ones)
- Old keys destroyed after successful rotation
- Public verification key remains unchanged
This allows:
- Adding new operators
- Removing compromised operators
- Changing threshold values (k, m)
- Maintaining service continuity
Identifier Obfuscation Process
The obfuscation process transforms a plaintext identifier into a privacy-preserving on-chain identifier.
Step-by-Step Process
1. Format the Identifier
Combine the identifier prefix with the plaintext identifier:
formatted = "{prefix}://{plaintextIdentifier}"
Example:
Phone: "PHONE_NUMBER://+12345678901"
Twitter: "twitter://@alice"
2. Hash the Formatted Identifier
hashedIdentifier = sha3(formatted)
3. Blind the Hash
The client generates a random blinding factor and blinds the hash:
blindedHash = blind(hashedIdentifier, blindingFactor)
This step ensures ODIS cannot see the actual identifier.
4. Query ODIS
Send the blinded hash to ODIS operators:
const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier(
plaintextIdentifier,
identifierPrefix,
issuerAddress,
authSigner,
serviceContext
);
5. ODIS Processing
Each ODIS operator:
- Receives the blinded hash
- Computes a partial signature using their key share
- Returns the blinded partial signature
6. Combine Signatures
When k signatures are received:
- Signatures are combined into a full signature
- This is the “blinded pepper signature”
7. Unblind the Signature
The client unblinds the signature:
unblinedSignature = unblind(blindedPepperSignature, blindingFactor)
8. Generate the Pepper
Extract the pepper from the unblinded signature:
pepper = first13Chars(sha256(unblindedSignature))
9. Create Obfuscated Identifier
Combine the hashed identifier with the pepper:
obfuscatedIdentifier = sha3(hashedIdentifier + "__" + pepper)
Final Formula:
obfuscatedIdentifier = sha3(sha3("{prefix}://{plaintext}") + "__" + pepper)
Verification
Results from ODIS can be verified against the service’s public key, which is shared with users through the client library.
Registration & Lookup Flow
Registration Flow
The process of creating an attestation mapping:
┌──────┐ ┌────────┐ ┌──────┐ ┌──────────────┐
│ User │ │ Issuer │ │ ODIS │ │ Blockchain │
└───┬──┘ └───┬────┘ └───┬──┘ └──────┬───────┘
│ │ │ │
│ 1. Request Verification│ │ │
├──────────────────────>│ │ │
│ │ │ │
│ 2. Verify Ownership │ │ │
│ (SMS, OAuth, etc.) │ │ │
│<─────────────────────>│ │ │
│ │ │ │
│ │ 3. Query for Pepper │ │
│ ├──────────────────────>│ │
│ │ │ │
│ │ 4. Return Pepper │ │
│ │<──────────────────────┤ │
│ │ │ │
│ │ 5. Register Attestation │
│ ├───────────────────────────────────────────────>│
│ │ │ │
│ │ 6. Attestation Registered │
│ │<───────────────────────────────────────────────┤
│ │ │ │
│ 7. Confirmation │ │ │
│<──────────────────────┤ │ │
Detailed Steps:
-
User Requests Verification
- User provides their identifier and wallet address to an issuer
- User indicates they want to create an attestation
-
Issuer Verifies Ownership
- Phone: Send SMS with verification code
- Twitter: OAuth flow
- Email: Verification link
- Custom: Any verification method the issuer chooses
-
Issuer Queries ODIS
- Issuer sends blinded identifier to ODIS
- Consumes issuer’s ODIS quota
- Receives pepper for obfuscation
-
Issuer Computes Obfuscated Identifier
- Combines hashed identifier with pepper
- Creates the final obfuscated identifier
-
Register Attestation On-Chain
await federatedAttestationsContract .registerAttestationAsIssuer( obfuscatedIdentifier, userAccountAddress, attestationVerifiedTime ) .send(); -
Gas Payment Options
- Issuer Pays: Issuer executes transaction with their gas
- User Pays: Issuer signs attestation, user submits transaction
Lookup Flow
The process of finding addresses from identifiers:
┌─────────────┐ ┌──────┐ ┌──────────────┐
│ Application │ │ ODIS │ │ Blockchain │
└──────┬──────┘ └───┬──┘ └──────┬───────┘
│ │ │
│ 1. Query for Pepper │ │
├────────────────────>│ │
│ │ │
│ 2. Return Pepper │ │
│<────────────────────┤ │
│ │ │
│ 3. Lookup Attestations │
├─────────────────────────────────────────>│
│ │ │
│ 4. Return Addresses │ │
│<─────────────────────────────────────────┤
Detailed Steps:
-
Get Obfuscated Identifier
const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier( plaintextIdentifier, identifierPrefix, lookupAddress, authSigner, serviceContext ); -
Query FederatedAttestations Contract
const attestations = await federatedAttestationsContract.lookupAttestations( obfuscatedIdentifier, [issuer1Address, issuer2Address, ...] ); -
Process Results
// Returns arrays for each issuer const { countsPerIssuer, // Number of attestations per issuer accounts, // Account addresses signers, // Signer addresses issuedOns, // Verification timestamps publishedOns // Registration timestamps } = attestations;
Multi-Issuer Lookup
Applications can query multiple issuers simultaneously:
const trustedIssuers = [
"0x6549aF2688e07907C1b821cA44d6d65872737f05", // Kaala
"0x388612590F8cC6577F19c9b61811475Aa432CB44" // Libera
];
const attestations = await federatedAttestationsContract.lookupAttestations(
obfuscatedIdentifier,
trustedIssuers
);
// Results are ordered by issuer
// attestations.countsPerIssuer[0] = number of attestations from Kaala
// attestations.countsPerIssuer[1] = number of attestations from Libera
Trust Model
Applications must decide which issuers to trust:
Single Issuer Trust:
- Trust only attestations from your own issuer
- Maximum control over verification quality
- Limited to your own user base
Multiple Issuer Trust:
- Trust attestations from multiple issuers
- Broader coverage and interoperability
- Must evaluate each issuer’s verification quality
Consensus-Based Trust:
- Require attestations from multiple issuers
- Higher confidence in verification
- Reduced coverage (fewer users will have multiple attestations)
Security Considerations
Privacy Assumptions
Self Connect’s privacy model assumes:
- ODIS Operators: At least k operators remain honest
- Rate Limiting: Quota system prevents mass scraping
- Blinding: Client-side blinding is implemented correctly
- No Collusion: Attackers don’t control k or more ODIS operators
If Assumptions Hold:
- Identifiers cannot be reverse-engineered from blockchain data
- Rainbow table attacks are prohibitively expensive
- User privacy is preserved
If Assumptions Fail:
- If k operators are compromised: Peppers for any identifier can be computed
- If rate limiting is bypassed: Rainbow tables become feasible
Sybil Resistance
Self Connect provides Sybil resistance through:
- Verification Requirements: Users must prove ownership of identifiers
- Issuer Quality: Applications choose issuers with strong verification
- Costly Registration: ODIS quota costs make mass fake registrations expensive
- Unique Identifiers: Phone numbers and verified social accounts are limited per person
Limitations:
- Relies on issuer verification quality
- Some identifiers (email) are easier to create in bulk
- Applications must choose issuers carefully
Comparison: ASv1 vs. Self Connect
| Aspect | ASv1 | Self Connect |
|---|---|---|
| Verification | 3 randomly selected validators | Issuer (flexible method) |
| Trust Model | Single root: Validator collective | Multiple roots: Each issuer |
| Verification Quality | Standardized across network | Varies by issuer |
| Flexibility | Phone numbers only | Any identifier type |
| Censorship Resistance | Validator majority | Multi-issuer selection |
| Scalability | Limited by validator overhead | Scales with issuer ecosystem |
Best Practices
- Choose Trusted Issuers
- Evaluate verification methods
- Check issuer reputation
- Monitor issuer behavior
- Implement Rate Limiting
- Limit lookup frequency per user
- Monitor for abnormal query patterns
- Validate Results
- Check timestamps for recency
- Verify issuer addresses
- Handle multiple attestations appropriately
- Secure Key Management
- Protect issuer private keys
- Use hardware security modules for production
- Implement key rotation procedures
- Monitor ODIS Quota
- Set up alerts for low quota
- Implement automatic top-ups
- Track quota usage patterns