Selective Disclosure
The Open Privacy Suite implements a selective disclosure system that allows users to share address visibility with authorized parties (auditors, regulators, compliance teams) while maintaining privacy controls.
Overview
The disclosure system provides:
- Granular control over what data is shared and how addresses are displayed
- Time-limited access with automatic expiration
- Audit trail of all disclosure access events
- Multiple disclosure levels from full transparency to complete redaction
Disclosure Levels
| Level | Address Display | Use Case |
|---|---|---|
| Full | Real addresses (0xa32c...94cd) | Regulatory subpoenas, law enforcement investigations |
| Pseudonymous | Consistent pseudonyms (Address-KDCM) | Financial audits -- allows pattern analysis without revealing identity |
| Redacted | Uniform [PRIVATE] placeholder for every address | Proof of activity -- timing, direction, gas, status visible, but no counterparty correlation across transactions |
The three levels form a privacy ladder:
- Full = identity + transaction graph.
- Pseudonymous = transaction graph without identity (same address renders as the same pseudonym within a grant, so patterns are visible).
- Redacted = volume and timing without graph (every address renders as the same placeholder, so the auditor cannot correlate counterparties between transactions).
valueis also withheld at this level; transaction hashes are never returned.
Activity log access (RPC method invocations as proof of activity, without addresses or parameters) is orthogonal to disclosure level — it is granted via the activity_logs or full_disclosure scope on the grant, not by the disclosure level.
Workflow
Data Models
Disclosure Request
A request from an authorized party to view someone's data:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"requester_did": "did:iden3:privado:main:2SaubQ6...",
"target_user_id": "user-123",
"org_id": "org-456",
"scope": {
"addresses": ["0xa32c..."],
"date_range": {
"start": "2024-01-01T00:00:00Z",
"end": "2024-12-31T23:59:59Z"
},
"disclosure_level": "pseudonymous"
},
"reason": "Annual financial audit",
"legal_basis": "GDPR Article 6(1)(c)",
"status": "pending",
"requested_at": "2024-06-15T10:30:00Z",
"expires_at": "2024-06-22T10:30:00Z"
}
Request Status Lifecycle
Disclosure Grant
An approved disclosure with access permissions:
{
"id": "grant-789",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"scope": {
"addresses": ["0xa32c..."],
"disclosure_level": "pseudonymous"
},
"granted_at": "2024-06-16T09:00:00Z",
"expires_at": "2024-07-16T09:00:00Z",
"revoked_at": null
}
Scope Configuration
The scope defines what data can be accessed:
| Field | Description |
|---|---|
methods | Specific RPC methods allowed (e.g., eth_call, eth_getLogs) |
addresses | Contract addresses to include in disclosure |
date_range | Time period for transaction/log data |
disclosure_level | How addresses are displayed (full / pseudonymous / redacted) |
Scope is enforced on the disclosed data. A grant with a date_range returns only transactions within that window, and a grant listing specific addresses returns only data for those contracts — activity outside the requested scope is never disclosed, regardless of the disclosure_level. Leaving a field unset means it is not narrowed (e.g. no date_range = the grant is not restricted by date).
API Endpoints
User Disclosure Dashboard
/api/v1/me/disclosure/requests?status=pendingList pending disclosure requests for your data
/api/v1/me/disclosure/grants?status=activeList active disclosure grants you have given
/api/v1/me/disclosure/requests/{id}/approveApprove a disclosure request
{
"grant_duration_hours": 720
}
grant_duration_hours is optional and defaults to 24.
/api/v1/me/disclosure/requests/{id}/rejectReject a disclosure request
{
"reason": "Insufficient justification provided"
}
Admin / Auditor Access
/api/v1/admin/disclosure/requestsCreate a new disclosure request
{
"target_user_id": "user-123",
"scope": {
"addresses": ["0xa32c..."],
"disclosure_level": "pseudonymous",
"date_range": {
"start": "2024-01-01T00:00:00Z",
"end": "2024-12-31T23:59:59Z"
}
},
"reason": "Annual audit",
"legal_basis": "GDPR Art. 6(1)(c)"
}
/api/v1/admin/disclosure/requestsList all disclosure requests (admin only). Supports status and target_user_id filters.
/api/v1/admin/disclosure/grants/{id}/revokeRevoke an active grant (admin only)
/api/v1/admin/disclosure/requests/{id}Delete a pending request (admin only)
Explorer Integration
When a user views disclosed data in the block explorer:
/api/v1/explorer/viewable-addresses?did=did:iden3:...Get addresses visible to the authenticated user
Response includes own addresses and disclosed addresses with their disclosure level:
{
"viewer_did": "did:iden3:...",
"own_addresses": [
{ "address": "0xabc..." }
],
"disclosed_addresses": [
{
"address": "Address-KDCM",
"address_id": "c311e533...",
"owner_did": "did:iden3:...",
"disclosure_level": "pseudonymous",
"grant_id": "265991a8-...",
"expires_at": "2024-07-16T09:00:00Z"
}
]
}
/api/v1/explorer/grant/{grant_id}/resolve/{address_id}View address details via grant (uses opaque address_id)
/api/v1/explorer/grant/{grant_id}/{address_id}/transactionsGet pseudonymized transactions for a disclosed address
Security Model
Address Protection
For pseudonymous and redacted disclosures:
- Real addresses are never sent to the frontend. The backend resolves
address_idto the real address internally. Only pseudonyms or[PRIVATE]are returned to clients. - Transaction hashes are hidden. This prevents lookup on other block explorers to find real addresses. Only block numbers, timestamps, and values are shown.
- Consistent pseudonyms within a grant. The same address always gets the same pseudonym (e.g.,
Address-KDCM). External addresses get pseudonyms likeExternal-7E56. This allows pattern analysis without revealing identity.
Scope Integrity
- Approval cannot widen the request. When approving a request, the approver can only narrow the requested
scope(tighten thedate_range, dropaddresses, lower thedisclosure_level) — never broaden it. A grant can never disclose more than the requester originally asked for and the data owner reviewed. - The grant's scope bounds every response. The
date_rangeandaddresseson the grant are enforced on all disclosed transaction and log data, so a viewer cannot reach activity outside the window or contracts they were granted.
Opaque Address IDs
To prevent address correlation, the system uses opaque address_id values:
address_id = SHA256(lowercase(address) + ":" + grant_id)[:16]
This ensures:
- Same address has different IDs across different grants
- Cannot reverse-engineer the real address from the ID
- Routes are secure:
/grant/{grant_id}/{address_id}
Fail-Safe Defaults
- Unknown disclosure levels default to redacted
- Missing viewer identity returns no disclosed addresses
- Expired or revoked grants return 403 Forbidden
Error responses are opaque
Every disclosure endpoint returns generic error bodies — never the underlying error type, SQL state, or token-validation reason. The same shape is used for "token expired", "token revoked", "token never existed", and "token belongs to another grant", so a client cannot use error messages to enumerate valid grant IDs or distinguish a banned token from a typo.
| Class | Status | Body |
|---|---|---|
| Missing or malformed token | 401 Unauthorized | {"error": "authentication required"} |
| Token rejected (any reason) | 401 Unauthorized | {"error": "invalid disclosure token"} |
| Caller may not perform this action | 403 Forbidden | {"error": "access denied"} |
| Grant or referenced resource does not exist | 404 Not Found | {"error": "grant not found"} |
| Malformed request body | 400 Bad Request | {"error": "invalid request body"} |
| Server-side failure | 500 Internal Server Error | {"error": "<operation> failed"} (generic; details in proxy logs only) |
Operators triage from slog.Error / slog.Warn log lines, which carry the full error context (grant_id, request_id, underlying error). The client side intentionally cannot tell the difference between adjacent failure modes.
Audit Trail
Every access to disclosed data is logged:
{
"grant_id": "grant-789",
"action": "view_transactions",
"resource_type": "transactions",
"viewer_ip": "192.168.1.100",
"accessed_at": "2024-06-20T14:30:00Z",
"data_summary": {
"record_count": 47,
"date_range": {
"start": "2024-01-01",
"end": "2024-06-20"
}
}
}
Report Types
The system supports generating compliance reports:
| Report Type | Description |
|---|---|
activity_summary | Aggregated activity statistics |
sanctions_check | Check for interactions with sanctioned addresses |
compliance_report | Full compliance audit report |
Configuration
Disclosure timeouts are set per request, not via environment variables:
- Request expiry — set per request via
expires_in_hoursin the create-request body (0= no expiration). - Grant duration — set per approval via
grant_duration_hours(default 24); there is no server-wide maximum-duration clamp.
Best Practices
- Always specify a reason and legal basis for disclosure requests.
- Use the minimum disclosure level needed -- prefer pseudonymous over full.
- Set appropriate expiration dates -- do not request indefinite access.
- Review the audit trail regularly for compliance.
- Revoke grants promptly when access is no longer needed.