-
-
Save denniswon/735b62f6f9ae6e248c2854c8239f2ea8 to your computer and use it in GitHub Desktop.
| ⏺ ★ Insight ───────────────────────────────────── | |
| The latest published SDK is 0.7.1. The HPKE privacy functions (createSecureEnvelope, uploadEncryptedData, getPrivacyPublicKey) were added in v0.5.0. The newton-identity app is on | |
| ^0.2.10 — way behind. We need to bump to ^0.7.1. | |
| The identity app's current flow is: | |
| 1. RSA-OAEP encrypt → hex string (encryptSecret) | |
| 2. EIP-712 sign the hex string ({ data: encryptedInquiryData }) | |
| 3. Upload via newt_uploadIdentityEncrypted with encrypted_data field | |
| After migration: | |
| 1. HPKE encrypt → SecureEnvelope JSON (createSecureEnvelope from SDK) | |
| 2. EIP-712 sign the envelope JSON string ({ data: envelopeJson }) | |
| 3. Upload via newt_uploadIdentityEncrypted with envelope field | |
| The gateway already expects envelope (from PR #489). The EIP-712 EncryptedIdentityData type signs { data: string } — so the data field just changes from hex RSA ciphertext to | |
| envelope JSON. | |
| ───────────────────────────────────────────────── | |
| Now let me implement the newton-identity changes. First, let me check what other files reference the encryption utilities. | |
| Searched for 1 pattern (ctrl+o to expand) | |
| ⏺ Good — encryptSecret is only used in register-user-data/page.tsx. The encrypt.ts utilities are exported but let me check if they're imported anywhere else. | |
| Searched for 1 pattern (ctrl+o to expand) | |
| ⏺ Only exported from utils/index.ts, never imported by any other file. Safe to delete both encryption files. Now let me implement all the changes. | |
| ⏺ oh-my-claudecode:executor(Migrate newton-identity to HPKE) | |
| ⎿ Done (16 tool uses · 106.5k tokens · 1m 17s) | |
| (ctrl+o to expand) | |
| ⏺ Let me verify the changes look correct. | |
| Read 2 files, listed 1 directory (ctrl+o to expand) | |
| ⏺ All changes verified: | |
| - page.tsx — clean HPKE flow with createSecureEnvelope, sends envelope field | |
| - utils/index.ts — encrypt exports removed | |
| - kms.ts and encrypt.ts — both deleted | |
| - package.json — SDK bumped to ^0.7.1 | |
| One thing I want to clean up — the hexToBytes conversion for the signing key is verbose. The SDK's hexToBytes from viem would be cleaner, but since this is a 'use client' | |
| component and we're already importing from the SDK, the manual conversion is fine. It works correctly. | |
| ⏺ Both repos are updated. Here's the summary: | |
| ★ Insight ───────────────────────────────────── | |
| newton-sdk (3 files changed): | |
| - StoreEncryptedSecretsParams now accepts plaintext: Record<string, unknown> + optional recipientPublicKey instead of secrets: string (base64 KMS ciphertext) | |
| - storeEncryptedSecrets() internally calls createSecureEnvelope to HPKE-encrypt, sends envelope field matching the gateway's PublishSecretsEncryptedRequest | |
| - Build passes | |
| newton-identity (5 files changed, 2 deleted): | |
| - Deleted src/lib/kms.ts (RSA-OAEP with env var key) and src/utils/encrypt.ts (RSA-OAEP with hardcoded key) | |
| - register-user-data/page.tsx now uses getPrivacyPublicKey + createSecureEnvelope from the SDK, sends envelope instead of encrypted_data | |
| - Bumped SDK from ^0.2.10 to ^0.7.1 | |
| - Removed NEXT_PUBLIC_KMS_PUBLIC_KEY dependency (no longer needed) | |
| ───────────────────────────────────────────────── | |
| Would you like me to: | |
| 1. Create commits and PRs for both repos? | |
| 2. Update the migration progress memory? | |
| 3. Both? | |
| ✻ Brewed for 5m 0s | |
| ❯ 3 | |
| ⏺ Let me do both in parallel — commits/PRs for both repos and update memory. | |
| First, let me check git status on both repos. | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-sdk && git status && echo "---BRANCH---" && git branch --show-current && echo "---LOG---" && git log --oneline -5) | |
| ⎿ On branch main | |
| Your branch is up to date with 'origin/main'. | |
| … +15 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-identity && git status && echo "---BRANCH---" && git branch --show-current && echo "---LOG---" && git log --oneline -5) | |
| ⎿ On branch main | |
| Your branch is up to date with 'origin/main'. | |
| … +18 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Both on main. Let me create feature branches, commit, push, and create PRs for both repos in parallel. I'll also update memory. | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-sdk && git checkout -b feat/kms-to-hpke-migration && git add src/modules/privacy/index.ts src/types/privacy.ts && git commi…) | |
| ⎿ Switched to a new branch 'feat/kms-to-hpke-migration' | |
| WARN Issue while reading "/Users/dennis.won/projects/newton-sdk/.npmrc". Failed to replace env in config: ${NPM_TOKEN} | |
| … +49 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-identity && git checkout -b feat/kms-to-hpke-migration && git add package.json src/app src/lib/kms.ts src/utils/encrypt.ts…) | |
| ⎿ Switched to a new branch 'feat/kms-to-hpke-migration' | |
| [feat/kms-to-hpke-migration dd347ca] feat: replace RSA-OAEP encryption with HPKE | |
| 5 files changed, 36 insertions(+), 162 deletions(-) | |
| … +2 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Commits created. The SDK lint warnings are pre-existing (in test files, not our changes). Now push and create PRs. | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-sdk && git push -u origin feat/kms-to-hpke-migration 2>&1) | |
| ⎿ remote: | |
| remote: Create a pull request for 'feat/kms-to-hpke-migration' on GitHub by visiting: | |
| remote: https://github.com/newt-foundation/newton-sdk/pull/new/feat/kms-to-hpke-migration | |
| … +7 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-identity && git push -u origin feat/kms-to-hpke-migration 2>&1) | |
| ⎿ remote: | |
| remote: Create a pull request for 'feat/kms-to-hpke-migration' on GitHub by visiting: | |
| remote: https://github.com/newt-foundation/newton-identity/pull/new/feat/kms-to-hpke-migration | |
| … +7 lines (ctrl+o to expand) | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Both pushed. Now create PRs and update memory in parallel. | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-sdk && gh pr create --title "feat: migrate storeEncryptedSecrets from KMS to HPKE" --body "$(cat <<'EOF' | |
| Replaces the KMS c…) | |
| ⎿ https://github.com/newt-foundation/newton-sdk/pull/146 | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ Bash(cd /Users/dennis.won/projects/newton-identity && gh pr create --title "feat: replace RSA-OAEP encryption with HPKE" --body "$(cat <<'EOF' | |
| Replaces client-side R…) | |
| ⎿ https://github.com/newt-foundation/newton-identity/pull/19 | |
| ⎿ Shell cwd was reset to /Users/dennis.won/projects/newton-prover-avs | |
| ⏺ PRs created: | |
| - newton-sdk: newt-foundation/newton-sdk#146 | |
| - newton-identity: newt-foundation/newton-identity#19 | |
| Now let me update the migration progress memory. | |
| Recalled 1 memory, wrote 1 memory (ctrl+o to expand) | |
| ⏺ All done. Here's the full status: | |
| PRs created: | |
| - AVS: newt-foundation/newton-prover-avs#489 (under review) | |
| - Deploy: newt-foundation/newton-prover-avs-deploy#188 (under review) | |
| - SDK: newt-foundation/newton-sdk#146 | |
| - Identity: newt-foundation/newton-identity#19 | |
| Deploy order (wire format changes must go together): | |
| 1. Generate encryption key: cast wallet new | |
| 2. Store in Secrets Manager as GATEWAY__ENCRYPTION__PRIVATE_KEY and OPERATOR__ENCRYPTION__PRIVATE_KEY | |
| 3. Merge deploy PR #188 → merge AVS PR #489 (deploy together) | |
| 4. Publish SDK (merge #146, version bump) | |
| 5. Merge identity PR #19 (depends on SDK publish) | |
| Last remaining task: NEWT-878 (cross-repo E2E validation) — blocked on merge + deploy of all 4 PRs. | |
| ✻ Churned for 1m 47s | |
| ❯ what is the ephemeralKey here? how does the user who uploaded the secrets later decrypt the secrets from the protocol by oneself? | |
| ⏺ Good question — let me clarify. | |
| ★ Insight ───────────────────────────────────── | |
| The user who uploads secrets does NOT decrypt them. The decryption flow is: | |
| 1. Client encrypts secrets with the gateway/operator's HPKE public key and uploads | |
| 2. Gateway decrypts to validate against the PolicyData schema, then stores the raw envelope | |
| 3. Operators fetch the envelope from DB and decrypt locally during policy evaluation | |
| The secrets are encrypted to the Newton infrastructure (gateway + operators share the same ENCRYPTION_PRIVATE_KEY). The client never needs to decrypt — they're providing secrets | |
| (like API keys) for policy evaluation, not retrieving them later. | |
| The ephemeral Ed25519 key in storeEncryptedSecrets is required by createSecureEnvelope's API (it signs the envelope), but the gateway ignores this signature for secrets. | |
| Ownership is validated via the API key + on-chain getOwner() check instead. So the ephemeral key is throwaway — generated, used once for the Ed25519 signature to satisfy the | |
| function signature, then zeroized. | |
| ───────────────────────────────────────────────── | |
| The data flow for secrets is one-directional: | |
| Client (policy owner) | |
| → HPKE encrypt to gateway's public key | |
| → Upload via newt_storeEncryptedSecrets | |
| → Gateway decrypts to validate schema | |
| → Gateway stores envelope in DB | |
| → Operators fetch envelope from DB + HPKE decrypt during policy eval | |
| The client doesn't need to decrypt because: | |
| - They already know the plaintext (they're the ones uploading it) | |
| - If they want to update secrets, they upload a new envelope (upsert by policy_client + policy_data_address) | |
| - There's no "download my secrets" RPC — secrets are write-only from the client's perspective | |
| Does this answer your concern, or were you asking about a different use case? |
⏺ ★ Insight ─────────────────────────────────────
Now I can see the full picture. While all three data types share the same encrypted_data_refs table, their RPC endpoints have
fundamentally different validation and authorization logic:
┌───────────────────────────────────────────────────┬───────────────┬──────────────────────────────────────┬─────────────────────────┐
│ Concern │ Privacy │ Identity │ Secrets │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Auth │ Ed25519 sig │ EIP-712 sig + gateway co-sign │ On-chain getOwner() │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Data ID │ UUID (random) │ Keccak256 (content-hash) │ DB row (upsert by key │
│ │ │ │ pair) │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Retrieval │ No API │ Yes (newt_getIdentityEncrypted) │ No API │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Schema validation │ None │ None │ Against on-chain │
│ │ │ │ PolicyData schema │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ On-chain registration │ No │ Yes (IdentityRegistry) │ No │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Response shape │ { data_ref_id │ { data_ref_id, gateway_signature, │ { schema } │
│ │ } │ deadline } │ │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ ───────────────────────────────────────────────── │ │ │ │
└───────────────────────────────────────────────────┴───────────────┴──────────────────────────────────────┴─────────────────────────┘
Here's my assessment on unification:
The upload paths are hard to unify cleanly. Each data type has different:
- Authorization mechanisms (Ed25519 vs EIP-712 vs on-chain ownership)
- Response shapes (UUID vs content-hash + gateway signature vs schema)
- Side effects (identity triggers gateway co-signing, secrets triggers schema validation)
- Idempotency semantics (privacy = insert new, identity = insert with content-hash dedup, secrets = upsert by key pair)
Forcing these into a single newt_uploadEncryptedData with a data_type discriminator would mean a union-type request with mostly-optional
fields, and a union-type response. The handler would be a big match that dispatches to the same logic we have now. Net result: more
complexity at the API surface for no real simplification.
The retrieval path could be unified — a single newt_getEncryptedData(data_ref_id) that returns the envelope regardless of type.
newt_getIdentityEncrypted already does this for identity. Extending it to all types is straightforward since they all share the same
table.
My recommendation:
- Keep separate upload endpoints — the auth/validation/response differences justify distinct APIs
- Add a unified retrieval endpoint newt_getEncryptedData(data_ref_id, chain_id) that works for any data type
- Deprecate newt_getIdentityEncrypted in favor of the unified getter (or alias it)
This gives clients a single way to retrieve any encrypted data while keeping the upload paths clean and type-safe. Does this approach
make sense, or do you still want a fully unified upload+retrieval API?
✻ Cooked for 1m 38s
❯ this makes sense. one question: for privacy data, why is it insert new not insert with content-hash dedup? what is the intrisic
difference between general privacy data vs identity data in terms of storage that requires this difference?
⏺ Good question. Let me check the actual implementation to give you an accurate answer.
Read 1 file (ctrl+o to expand)
⏺ The difference comes down to how the data is used downstream, not an intrinsic storage difference.
Privacy data uses UUIDs because the same user can upload multiple distinct encrypted values for the same policy context — e.g., a bank
balance encrypted once, then a separate portfolio value encrypted again. Each upload gets its own UUID, and the client passes a list of
encrypted_data_ref_ids in newt_createTask. Content-hash dedup would be wrong here: two uploads with identical plaintext at different
times are still separate data references (they might represent different snapshots).
Identity data uses content-hash because it's idempotent by design — the same identity data (birthdate, country, etc.) for the same owner
should always produce the same data_ref_id. This data_ref_id gets registered on-chain in the IdentityRegistry contract. If the user
re-uploads the same data, they get the same hash back, and the on-chain registration still points to the right blob. Content-addressing
is critical here because the on-chain contract stores the data_ref_id permanently.
But honestly — this distinction is somewhat accidental rather than deeply principled. Privacy data could also use content-hash if we
wanted dedup. The real driver was:
- Privacy refs are ephemeral (TTL-based, used once per task) → UUID is simpler
- Identity refs are persistent (registered on-chain, referenced across multiple tasks) → content-hash enables deterministic re-upload
without creating orphans
If we're thinking about unifying the retrieval API, this distinction doesn't matter — both UUIDs and content-hashes are just opaque
string keys into the same table. The unified newt_getEncryptedData(data_ref_id) would work for either.
Want me to file a Linear ticket for the unified retrieval endpoint, or save this for a later planning session?
❯ what is the use case difference between general privacy data in the scope of the protocol? what is the entire e2e flow, life cycle from
user encrypted and uploaded the private data to be used during policy evaluation?
⏺ Great question — let me trace the full lifecycle for each data type. Let me check how privacy data actually flows through task creation
and evaluation.
Searched for 2 patterns (ctrl+o to expand)
⏺ Now let me check how privacy data flows into newt_createTask and how identity data is used during evaluation.
Searched for 2 patterns (ctrl+o to expand)
⏺ Now I have the full picture. Here are the complete E2E lifecycles:
★ Insight ─────────────────────────────────────
All three data types share the same fundamental pattern: client encrypts → gateway stores → operator decrypts during evaluation. The
difference is in when the data is bound to a task and how it reaches the Rego engine.
─────────────────────────────────────────────────
Full E2E Lifecycles
Privacy Data (general-purpose, per-task)
-
CLIENT: Upload phase (before task creation)
→ createSecureEnvelope(plaintext, policyClient, chainId)
→ newt_uploadEncryptedData → gets back data_ref_id (UUID)
→ Can upload multiple refs (e.g., bank balance + portfolio) -
CLIENT: Task creation (binds refs to task)
→ newt_createTask({ ..., encrypted_data_refs: [uuid1, uuid2],
user_signature, app_signature })
→ Dual Ed25519 signatures prevent unauthorized ref usage -
GATEWAY: Fetches ref records from DB, passes encrypted_data_ref_ids
to operators via ConsensusCommitRequest (no decryption) -
OPERATOR: decrypt_refs_from_db()
→ Fetches envelopes from DB by UUID
→ HPKE-decrypts each locally
→ Merges into Rego namespace: data.data.privacy -
OPERATOR: Evaluates Rego policy with privacy data available
→ BLS signs the result
→ Privacy data NEVER in policyTaskData (BLS-signed, on-chain)
Use case: Ephemeral, per-task sensitive data — bank balances, portfolio values, transaction details. Each upload is unique (UUID).
TTL-based expiration. User provides data at task time.
Identity Data (persistent, per-user)
-
CLIENT: Upload phase (one-time registration)
→ createSecureEnvelope(identityData, identityDomain, chainId)
→ newt_uploadIdentityEncrypted → gets back data_ref_id (keccak256 hash)- gateway_signature + deadline
-
CLIENT: On-chain registration
→ IdentityRegistry.registerIdentityData(domain, data_ref_id, gatewaySig, deadline)
→ data_ref_id stored permanently on-chain -
CLIENT: Link identity to policy client
→ IdentityRegistry.linkIdentity(policyClient, domain) -
OPERATOR: During ANY task evaluation for that policy client
→ resolve_identity_data() reads IdentityRegistry on-chain
→ Gets data_ref_id from contract
→ resolve_identity_data_ref() fetches encrypted blob from DB
→ decrypt_identity_data() HPKE-decrypts
→ Domain dispatch (deserialize_identity_data) → typed Rego object
→ Injected into Rego: data.identity.{domain} (e.g., data.identity.persona_kyc)
Use case: Long-lived personal data — KYC status, birthdate, country. Registered once, used across many tasks. Content-hash dedup because
re-uploading same data should yield same on-chain ref.
Secrets (persistent, per-PolicyData)
-
CLIENT: Upload phase (one-time per PolicyData)
→ storeEncryptedSecrets({ plaintext: { API_KEY: "..." }, policyClient, ... })
→ Gateway decrypts to validate against on-chain schema, stores envelope
→ Upserts by (policy_client, policy_data_address) — idempotent -
OPERATOR: During ANY task evaluation for that policy client
→ WASM execution fetches secrets from DB
→ HPKE-decrypts locally
→ Secrets injected into WASM execution context (not Rego directly)
→ WASM plugin uses secrets to call external APIs (e.g., exchange rate API)
Use case: API keys, service credentials for WASM plugins. Set once by policy owner, used on every task. No on-chain registration needed —
scoped to PolicyData contract.
The Actual Difference
┌────────────────┬──────────────────────────┬─────────────────────────────┬─────────────────────────────┐
│ Aspect │ Privacy │ Identity │ Secrets │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Lifecycle │ Ephemeral (per-task) │ Persistent (per-user) │ Persistent (per-PolicyData) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Who provides │ End user (intent sender) │ Identity owner (KYC user) │ Policy owner (developer) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ When bound │ At task creation time │ At identity registration │ At PolicyData setup │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ On-chain ref │ No │ Yes (IdentityRegistry) │ No │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Rego namespace │ data.data.privacy │ data.identity.{domain} │ N/A (WASM context) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Multiplicity │ Many refs per task │ One ref per (owner, domain) │ One per (client, data_addr) │
└────────────────┴──────────────────────────┴─────────────────────────────┴─────────────────────────────┘
The intrinsic difference between privacy and identity isn't really about UUID vs content-hash — it's about binding time. Privacy data is
bound at task creation (ephemeral, user-controlled). Identity data is bound at registration time and automatically included in every
subsequent task for that policy client (persistent, protocol-controlled).
You're right that they could share more infrastructure. A unified retrieval endpoint makes sense. The upload paths genuinely differ
because of the authorization and binding semantics, but the storage and retrieval layer is already unified (same table, same HPKE, same
operator decryption path).
Want me to capture this analysis somewhere — a doc update, Linear ticket for the unified retrieval API, or both?
also right now we have newt_uploadIdentityEncrypted/newt_getIdentityEncrypted and also newt_storeEncryptedSecrets. Since now identity
data, plicy client secrets, and other privacy data share the same storage, get we make it one set of send vs request api methods that
support different data types? /clarify-plan
⏺ Good question — this is about unifying the upload/retrieval RPC surface now that all encrypted data lives in the same encrypted_data_refs
table with a data_type discriminator.
Let me understand the current state and constraints before proposing anything.
⏺ Explore(Explore current RPC endpoints)
⎿ Done (23 tool uses · 86.3k tokens · 53s)
(ctrl+o to expand)
⏺ ★ Insight ─────────────────────────────────────
Now I can see the full picture. While all three data types share the same encrypted_data_refs table, their RPC endpoints have
fundamentally different validation and authorization logic:
┌───────────────────────────────────────────────────┬───────────────┬──────────────────────────────────────┬─────────────────────────┐
│ Concern │ Privacy │ Identity │ Secrets │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Auth │ Ed25519 sig │ EIP-712 sig + gateway co-sign │ On-chain getOwner() │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Data ID │ UUID (random) │ Keccak256 (content-hash) │ DB row (upsert by key │
│ │ │ │ pair) │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Retrieval │ No API │ Yes (newt_getIdentityEncrypted) │ No API │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Schema validation │ None │ None │ Against on-chain │
│ │ │ │ PolicyData schema │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ On-chain registration │ No │ Yes (IdentityRegistry) │ No │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ Response shape │ { data_ref_id │ { data_ref_id, gateway_signature, │ { schema } │
│ │ } │ deadline } │ │
├───────────────────────────────────────────────────┼───────────────┼──────────────────────────────────────┼─────────────────────────┤
│ ───────────────────────────────────────────────── │ │ │ │
└───────────────────────────────────────────────────┴───────────────┴──────────────────────────────────────┴─────────────────────────┘
Here's my assessment on unification:
The upload paths are hard to unify cleanly. Each data type has different:
- Authorization mechanisms (Ed25519 vs EIP-712 vs on-chain ownership)
- Response shapes (UUID vs content-hash + gateway signature vs schema)
- Side effects (identity triggers gateway co-signing, secrets triggers schema validation)
- Idempotency semantics (privacy = insert new, identity = insert with content-hash dedup, secrets = upsert by key pair)
Forcing these into a single newt_uploadEncryptedData with a data_type discriminator would mean a union-type request with mostly-optional
fields, and a union-type response. The handler would be a big match that dispatches to the same logic we have now. Net result: more
complexity at the API surface for no real simplification.
The retrieval path could be unified — a single newt_getEncryptedData(data_ref_id) that returns the envelope regardless of type.
newt_getIdentityEncrypted already does this for identity. Extending it to all types is straightforward since they all share the same
table.
My recommendation:
- Keep separate upload endpoints — the auth/validation/response differences justify distinct APIs
- Add a unified retrieval endpoint newt_getEncryptedData(data_ref_id, chain_id) that works for any data type
- Deprecate newt_getIdentityEncrypted in favor of the unified getter (or alias it)
This gives clients a single way to retrieve any encrypted data while keeping the upload paths clean and type-safe. Does this approach
make sense, or do you still want a fully unified upload+retrieval API?
✻ Cooked for 1m 38s
❯ this makes sense. one question: for privacy data, why is it insert new not insert with content-hash dedup? what is the intrisic
difference between general privacy data vs identity data in terms of storage that requires this difference?
⏺ Good question. Let me check the actual implementation to give you an accurate answer.
Read 1 file (ctrl+o to expand)
⏺ The difference comes down to how the data is used downstream, not an intrinsic storage difference.
Privacy data uses UUIDs because the same user can upload multiple distinct encrypted values for the same policy context — e.g., a bank
balance encrypted once, then a separate portfolio value encrypted again. Each upload gets its own UUID, and the client passes a list of
encrypted_data_ref_ids in newt_createTask. Content-hash dedup would be wrong here: two uploads with identical plaintext at different
times are still separate data references (they might represent different snapshots).
Identity data uses content-hash because it's idempotent by design — the same identity data (birthdate, country, etc.) for the same owner
should always produce the same data_ref_id. This data_ref_id gets registered on-chain in the IdentityRegistry contract. If the user
re-uploads the same data, they get the same hash back, and the on-chain registration still points to the right blob. Content-addressing
is critical here because the on-chain contract stores the data_ref_id permanently.
But honestly — this distinction is somewhat accidental rather than deeply principled. Privacy data could also use content-hash if we
wanted dedup. The real driver was:
- Privacy refs are ephemeral (TTL-based, used once per task) → UUID is simpler
- Identity refs are persistent (registered on-chain, referenced across multiple tasks) → content-hash enables deterministic re-upload
without creating orphans
If we're thinking about unifying the retrieval API, this distinction doesn't matter — both UUIDs and content-hashes are just opaque
string keys into the same table. The unified newt_getEncryptedData(data_ref_id) would work for either.
Want me to file a Linear ticket for the unified retrieval endpoint, or save this for a later planning session?
❯ what is the use case difference between general privacy data in the scope of the protocol? what is the entire e2e flow, life cycle from
user encrypted and uploaded the private data to be used during policy evaluation?
⏺ Great question — let me trace the full lifecycle for each data type. Let me check how privacy data actually flows through task creation
and evaluation.
Searched for 2 patterns (ctrl+o to expand)
⏺ Now let me check how privacy data flows into newt_createTask and how identity data is used during evaluation.
Searched for 2 patterns (ctrl+o to expand)
⏺ Now I have the full picture. Here are the complete E2E lifecycles:
★ Insight ─────────────────────────────────────
All three data types share the same fundamental pattern: client encrypts → gateway stores → operator decrypts during evaluation. The
difference is in when the data is bound to a task and how it reaches the Rego engine.
─────────────────────────────────────────────────
Full E2E Lifecycles
Privacy Data (general-purpose, per-task)
-
CLIENT: Upload phase (before task creation)
→ createSecureEnvelope(plaintext, policyClient, chainId)
→ newt_uploadEncryptedData → gets back data_ref_id (UUID)
→ Can upload multiple refs (e.g., bank balance + portfolio) -
CLIENT: Task creation (binds refs to task)
→ newt_createTask({ ..., encrypted_data_refs: [uuid1, uuid2],
user_signature, app_signature })
→ Dual Ed25519 signatures prevent unauthorized ref usage -
GATEWAY: Fetches ref records from DB, passes encrypted_data_ref_ids
to operators via ConsensusCommitRequest (no decryption) -
OPERATOR: decrypt_refs_from_db()
→ Fetches envelopes from DB by UUID
→ HPKE-decrypts each locally
→ Merges into Rego namespace: data.data.privacy -
OPERATOR: Evaluates Rego policy with privacy data available
→ BLS signs the result
→ Privacy data NEVER in policyTaskData (BLS-signed, on-chain)
Use case: Ephemeral, per-task sensitive data — bank balances, portfolio values, transaction details. Each upload is unique (UUID).
TTL-based expiration. User provides data at task time.
Identity Data (persistent, per-user)
-
CLIENT: Upload phase (one-time registration)
→ createSecureEnvelope(identityData, identityDomain, chainId)
→ newt_uploadIdentityEncrypted → gets back data_ref_id (keccak256 hash)- gateway_signature + deadline
-
CLIENT: On-chain registration
→ IdentityRegistry.registerIdentityData(domain, data_ref_id, gatewaySig, deadline)
→ data_ref_id stored permanently on-chain -
CLIENT: Link identity to policy client
→ IdentityRegistry.linkIdentity(policyClient, domain) -
OPERATOR: During ANY task evaluation for that policy client
→ resolve_identity_data() reads IdentityRegistry on-chain
→ Gets data_ref_id from contract
→ resolve_identity_data_ref() fetches encrypted blob from DB
→ decrypt_identity_data() HPKE-decrypts
→ Domain dispatch (deserialize_identity_data) → typed Rego object
→ Injected into Rego: data.identity.{domain} (e.g., data.identity.persona_kyc)
Use case: Long-lived personal data — KYC status, birthdate, country. Registered once, used across many tasks. Content-hash dedup because
re-uploading same data should yield same on-chain ref.
Secrets (persistent, per-PolicyData)
-
CLIENT: Upload phase (one-time per PolicyData)
→ storeEncryptedSecrets({ plaintext: { API_KEY: "..." }, policyClient, ... })
→ Gateway decrypts to validate against on-chain schema, stores envelope
→ Upserts by (policy_client, policy_data_address) — idempotent -
OPERATOR: During ANY task evaluation for that policy client
→ WASM execution fetches secrets from DB
→ HPKE-decrypts locally
→ Secrets injected into WASM execution context (not Rego directly)
→ WASM plugin uses secrets to call external APIs (e.g., exchange rate API)
Use case: API keys, service credentials for WASM plugins. Set once by policy owner, used on every task. No on-chain registration needed —
scoped to PolicyData contract.
The Actual Difference
┌────────────────┬──────────────────────────┬─────────────────────────────┬─────────────────────────────┐
│ Aspect │ Privacy │ Identity │ Secrets │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Lifecycle │ Ephemeral (per-task) │ Persistent (per-user) │ Persistent (per-PolicyData) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Who provides │ End user (intent sender) │ Identity owner (KYC user) │ Policy owner (developer) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ When bound │ At task creation time │ At identity registration │ At PolicyData setup │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ On-chain ref │ No │ Yes (IdentityRegistry) │ No │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Rego namespace │ data.data.privacy │ data.identity.{domain} │ N/A (WASM context) │
├────────────────┼──────────────────────────┼─────────────────────────────┼─────────────────────────────┤
│ Multiplicity │ Many refs per task │ One ref per (owner, domain) │ One per (client, data_addr) │
└────────────────┴──────────────────────────┴─────────────────────────────┴─────────────────────────────┘
The intrinsic difference between privacy and identity isn't really about UUID vs content-hash — it's about binding time. Privacy data is
bound at task creation (ephemeral, user-controlled). Identity data is bound at registration time and automatically included in every
subsequent task for that policy client (persistent, protocol-controlled).
You're right that they could share more infrastructure. A unified retrieval endpoint makes sense. The upload paths genuinely differ
because of the authorization and binding semantics, but the storage and retrieval layer is already unified (same table, same HPKE, same
operator decryption path).
Want me to capture this analysis somewhere — a doc update, Linear ticket for the unified retrieval API, or both?
✻ Worked for 55s
❯ there is a fundamental misalignment in terms of the privacy data binding time. The intended usage should be similar to identity data,
persistent with expiration time, uploaded before task creation, reusable, rego namespace to be similar data.privacy so that the
decrypted private data is NOT included in TaskResopnse that is publicly posted onchain. Also, multiplicity supported. Essentially, privacy
layer was intended to be designed to have similar flow as Identity data. The main difference is that the generic privacy data is uploaded
by the policy clients/their owner(s). The example of policy client owner(s) uploaded data is a shared blacklist: imagine uber and lyft
sharing the same co-contributed shared set of blacklisted user sets - thus, there needs to be similar onchain registry keeping track of
links from privacy data ref id to a set of policy clients, where the linked policy clients are the ones that will be able to use the
privacy data for their policies. Let's fix this fundamental architecture design for the generic privacy data use case support/flow.
/clarify-plan
⏺ This is a significant architectural insight. Let me make sure I understand the vision correctly before proposing changes.
★ Insight ─────────────────────────────────────
The current privacy data flow was designed as ephemeral per-task data (UUID, TTL, bound at task creation). But the intended architecture
is much closer to identity data — persistent, reusable, linked to policy clients via an on-chain registry. The key innovation is that
privacy data is contributed by policy client owners (not end users), and can be shared across multiple policy clients (e.g., a blacklist
co-contributed by Uber and Lyft).
─────────────────────────────────────────────────
Let me clarify the requirements by asking a few questions:
- On-chain registry: You're describing something like a PrivacyDataRegistry contract (analogous to IdentityRegistry) that tracks:
- data_ref_id → set of linked policy_client addresses
- Only linked policy clients can access the data during evaluation
- Multiple policy clients can link to the same data_ref_id
- The uploader (policy client owner) controls who gets linked
Is that right, or should linking be bidirectional (both the data owner AND the policy client must consent)?
-
Multiplicity: A single policy client can have multiple privacy data refs linked (e.g., blacklist A + sanctions list B). During
evaluation, the operator resolves ALL linked refs for that policy client and injects them into data.privacy as an array or merged object.
Correct? -
Content-hash vs UUID: Since privacy data is now persistent and reusable (like identity), should it also use content-hash
(keccak256(envelope)) as the data_ref_id? This gives dedup — re-uploading the same blacklist yields the same ref. -
Upload authorization: The current dual Ed25519 signature (user + app) at task creation time was designed for per-task binding. With
persistent data, the upload authorization shifts to on-chain ownership verification (like secrets) — the uploader must be getOwner() of
at least one policy client. Is that the right auth model, or should any address be able to upload and then link? -
Expiration: You mentioned "persistent with expiration time." Is this a TTL set at upload time (like current privacy data), or an
on-chain expiration managed by the registry (like identity's deadline for unconfirmed refs)? -
The current per-task ephemeral flow — should it be removed entirely, or kept as a separate "inline privacy data" mode for cases where
persistent registration is overkill?
❯ sorry, I was confused. So here is clarification. there is no difference between generic privacy data vs identity data. identity data
"is" the privacy data. verifiable credential project and newton privacy layer project were developed in parallel, and the design of how
newton verifiable credential identity data flows through the lifecycle is the correct one. Per-task ephemeral flow is a great use case as
"inline privacy data" mode, but we should not need to then persist the data into our database right? User can just upload the encrypted
data when submitting task (in wasm_args, we can have a special "_privacy" field where the ecrypted blob will live.)
⏺ Now I understand the full picture. Let me restate to confirm:
★ Insight ─────────────────────────────────────
There are actually only two modes, not three:
- Persistent encrypted data (identity = privacy = same thing): Uploaded once, registered on-chain, linked to policy clients, reusable
across tasks. The identity/VC flow is the canonical implementation. "Privacy data" as a separate persistent concept doesn't need to exist
— it IS identity data. - Inline ephemeral privacy data: Encrypted blob passed directly in the task request (e.g., via a _privacy field in wasm_args). No DB
persistence needed. The operator decrypts it in-memory during evaluation, uses it, discards it. This is for one-shot sensitive data that
doesn't need to be reused.
─────────────────────────────────────────────────
This simplifies things significantly:
Current state (broken):
- newt_uploadEncryptedData → persists ephemeral data to DB with UUID → gateway passes ref IDs to operators → operators fetch from DB and
decrypt - This is a worst-of-both-worlds design: persistent storage for ephemeral data
Proposed fix:
- Persistent path: Use the identity/VC flow (on-chain registry, content-hash refs, linked policy clients). If someone needs persistent
privacy data, they register it as identity data under the appropriate domain. - Ephemeral path: Pass encrypted blob inline in wasm_args._privacy (or a dedicated field on CreateTaskRequest). Operator HPKE-decrypts
in-memory, injects into data.data.privacy Rego namespace. No DB write, no ref IDs, no dual signatures. The envelope is carried with the
task itself.
What this means for existing code:
- newt_uploadEncryptedData and the encrypted_data_refs privacy flow become unnecessary for the ephemeral case
- The dual Ed25519 signature auth (privacy_auth.rs) is also unnecessary if the encrypted blob travels inline
- The encrypted_data_refs table's privacy data_type rows and the decrypt_refs_from_db operator path can be simplified or removed
Before I go further — is this the right understanding? And is this something you want to redesign now as part of this migration, or file
as a separate architectural task? It's a bigger change than the KMS→HPKE migration scope.
✻ Worked for 33s
❯ for peristent path, we always need dual signature verification - don't we already do this for identity data as well?
⏺ Good point — let me check what auth the identity upload path actually uses.
Read 2 files (ctrl+o to expand)
⏺ You're right — the auth models are different but serve the same purpose (preventing unauthorized use of encrypted data):
┌────────────────┬────────────────────────────────────────────┬─────────────────────────────────────────────────────────┐
│ │ Identity │ Privacy (current) │
├────────────────┼────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ At upload time │ EIP-712 signature by identity_owner │ Ed25519 signature by sender │
├────────────────┼────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ At task time │ Automatic (on-chain IdentityRegistry link) │ Dual Ed25519 (user + app) signatures in newt_createTask │
├────────────────┼────────────────────────────────────────────┼─────────────────────────────────────────────────────────┤
│ Binding │ On-chain registry link (persistent) │ Per-task ref IDs + signatures (ephemeral) │
└────────────────┴────────────────────────────────────────────┴─────────────────────────────────────────────────────────┘
For the persistent path, the identity flow already has authorization:
- Upload: EIP-712 proves the identity owner uploaded it
- Usage: On-chain IdentityRegistry.linkIdentity() controls which policy clients can access the data — the operator checks this link
on-chain before decrypting
The dual Ed25519 signature in privacy_auth.rs serves the same purpose for the ephemeral flow — it binds specific encrypted_data_ref_ids
to a specific task with consent from both the user and the app. This is necessary because there's no on-chain registry for ephemeral
data.
So to answer your question: yes, the persistent path already has authorization via on-chain registry links. The dual Ed25519 signatures
would only be needed for the inline ephemeral path (if we keep it), since there's no on-chain registry to control access there.
Actually — for inline ephemeral data passed in wasm_args._privacy, even dual signatures might be unnecessary. The data is encrypted to
the operator's HPKE key and travels with the task itself. The person submitting the task is explicitly providing the data for that task's
evaluation. The authorization is implicit in the act of submitting.
So the architecture simplifies to:
- Persistent → identity/VC flow with on-chain registry (already built)
- Ephemeral → inline in task request, no DB, no signatures needed beyond the task submission itself
Should I file this as an architectural redesign ticket, or do you want to start scoping the implementation now?
❯ let's compare the dual signature and linking methodologies between the current identity data vs dual ed25519 signatures: what are pros
vs cons? why was dual ed25519 signature used in the first place?
⏺ Good question. Let me read both implementations fully to give an accurate comparison.
Searched for 1 pattern, read 1 file (ctrl+o to expand)
⏺ Now let me check the identity registry's linking mechanism for the full comparison.
Read 1 file (ctrl+o to expand)
⎿ Loaded .claude/rules/solidity.md
⏺ Now I have the full picture of both authorization models. Here's the comparison:
Dual Ed25519 Signatures (Privacy — current)
How it works:
- At upload: sender signs envelope with Ed25519 (proves who uploaded)
- At task time: user signs keccak256(policy_client + intent_hash + ref_ids...), app counter-signs keccak256(policy_client + intent_hash +
user_signature) - Gateway verifies both signatures off-chain before broadcasting
Why it was designed this way:
The privacy layer was built for the ephemeral per-task use case. There's no on-chain registration, so there's no on-chain access control.
The dual-signature scheme was the off-chain substitute — it proves both the user (data owner) AND the app (policy client) consented to
using specific data refs for a specific task. It's essentially an off-chain authorization protocol.
Pros:
- No gas cost — entirely off-chain verification
- Per-task granularity — consent is scoped to a specific intent hash + specific refs
- No contract deployment needed
- User can selectively authorize which refs to use per task
Cons:
- Off-chain only — no on-chain audit trail of who authorized what
- Gateway is a trusted intermediary (if gateway is compromised, signatures can be bypassed)
- No revocation — once signed, the signature is valid until the task is processed
- No multi-policy-client sharing — authorization is per-task, not per-data-ref
- Ed25519 key management burden on both user and app (separate from their ECDSA wallet keys)
- No discoverability — no on-chain record of which refs exist or who can use them
On-Chain Registry Linking (Identity — current)
How it works:
- At upload: identity owner EIP-712 signs the envelope (standard wallet signature)
- Gateway co-signs with task generator key (proves gateway authorized the upload)
- On-chain: registerIdentityData(domain, dataRefId, gatewaySig, deadline) — stores ref on-chain
- Linking: linkIdentity(policyClient, clientUser, domains) — requires signatures from both the identity owner AND the client user (or via
convenience methods linkIdentityAsSigner/linkIdentityAsUser) - Access control: policyClientLinks[policyClient][clientUser][domain] → identityOwner
- At task time: operator reads on-chain link, fetches encrypted blob from DB, decrypts
Why it was designed this way:
Identity data is long-lived and shared across many tasks. The on-chain registry provides persistent, verifiable, revocable access
control. The linking mechanism ensures both parties consent (identity owner + policy client user), and
PolicyClientRegistry.isRegisteredClient() prevents linking to unregistered/deactivated clients.
Pros:
- On-chain audit trail — who registered what, who linked to whom
- Revocable — unlinkIdentity removes access
- Multi-policy-client sharing — one identity ref can be linked to N policy clients
- Persistent — survives across tasks without re-authorization
- Uses existing wallet keys (EIP-712/ECDSA) — no separate Ed25519 key management
- Discoverability — anyone can query the registry for linked data
- Contract-enforced access control — not dependent on gateway honesty
Cons:
- Gas cost for registration and linking transactions
- Requires contract deployment (IdentityRegistry)
- More complex setup flow (register → link → then use)
- Not suitable for truly ephemeral data
Summary
┌──────────────────────┬────────────────────────┬──────────────────────────────┐
│ Aspect │ Dual Ed25519 (Privacy) │ On-Chain Registry (Identity) │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Gas cost │ Zero │ Registration + linking txs │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Audit trail │ None (off-chain only) │ Full on-chain history │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Revocation │ No │ Yes (unlinkIdentity) │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Multi-client sharing │ No (per-task binding) │ Yes (link N clients) │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Key management │ Separate Ed25519 keys │ Existing wallet keys │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Trust model │ Trust gateway │ Trust-minimized (on-chain) │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Granularity │ Per-task + per-intent │ Per-data-ref + per-client │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Persistence │ Ephemeral │ Persistent │
├──────────────────────┼────────────────────────┼──────────────────────────────┤
│ Discoverability │ None │ On-chain queryable │
└──────────────────────┴────────────────────────┴──────────────────────────────┘
Bottom Line
The on-chain registry model is strictly better for persistent, reusable data — which is what you've clarified privacy data should be. The
dual Ed25519 model was designed for a use case (ephemeral per-task data) that should instead be handled by inline data in the task
request (no DB persistence).
For the inline ephemeral path, we don't need dual signatures at all — the encrypted blob travels with the task itself, and the person
submitting the task is implicitly authorizing its use. The HPKE encryption ensures only operators can read it.
So the architectural simplification is:
- Persistent encrypted data → on-chain registry (identity model, already built)
- Ephemeral encrypted data → inline in task request, no signatures needed
Want me to create a Linear ticket capturing this architectural redesign? It would include removing newt_uploadEncryptedData,
privacy_auth.rs, and the decrypt_refs_from_db path, and adding an inline _privacy field to the task request for the ephemeral case.
⏺ Design doc written at .omc/plans/privacy-data-architecture-redesign.md. Here's the summary:
★ Insight ─────────────────────────────────────
The plan has three phases:
Phase 1 (concrete, implementable now): Add inline_encrypted_data field to CreateTaskRequest for ephemeral per-task privacy data. No DB
persistence, no signatures, no ref IDs. Just encrypted blobs that travel with the task.
Phase 2 (cleanup): Deprecate newt_uploadEncryptedData, remove dead dual-signature code, clean up
encrypted_data_refs/user_signature/app_signature fields from task request.
Phase 3 (future, when needed): Persistent privacy data reuses the identity registry — new domain hashes (newton.privacy.blacklist, etc.),
Rego namespace routing, SDK convenience wrappers.
─────────────────────────────────────────────────
Please review the plan at .omc/plans/privacy-data-architecture-redesign.md. Key decisions I'd like your input on:
- Rego namespace: data.data.privacy (current) vs data.privacy (cleaner)?
- Inline data size limit: Should we cap envelope size?
- Phase 3 timing: Do we need persistent privacy-as-identity now, or wait for a concrete use case?
- Phase 1 scope for the SDK PR: Should we include Phase 1 SDK changes (the createInlinePrivacyData() function) in
newt-foundation/newton-sdk#146, or a separate PR?
Once you approve the plan, I'll create the Linear tickets under the Newton Privacy Layer project.
✻ Churned for 4m 3s
❯ For Phase 1, instead of adding anoother new field inline_encrypted_data, what do you think about changing wasm_args to just args where
args value is a "typed" struct where the key includes "wasm" for the args to wasm, "privacy" for the non-persistent privacy blob, and
"privacyRef" for the previously uploaded private data ref id (in case the size is over the inline limit)? And for phase 2, I think
Identity should be renamed to more generally, like privacy, and the overall APIs named/designed generic, where identity is accesses
newton.privacy.kyc, etc. What do you think? 1. data.privacy. 2. yes, let's cap it. what do you recommend as the limit? 3. phase 3 now.
Let's do more detailed planning regarding this. what do you mean by persistent privacy as identity? 4. include Phase 1 SDK changes (the
createInlinePrivacyData() function) in newt-foundation/newton-sdk#146
⏺ These are great refinements. Let me address each point:
On args as a typed struct: This is much cleaner than adding yet another field. Instead of wasm_args: Bytes + inline_encrypted_data:
Vec + encrypted_data_refs: Vec, you have one unified args field with typed keys. This also future-proofs for other data
types without growing the request struct.
On renaming Identity → Privacy: This aligns with your earlier insight that identity data IS privacy data. The IdentityRegistry contract
becomes the PrivacyRegistry (or we keep the contract name but rebrand the API layer). data.identity.persona_kyc becomes data.privacy.kyc,
and data.privacy.blacklist lives alongside it naturally.
On size limit: Given that HPKE envelopes include a 32-byte enc + ciphertext + 16-byte Poly1305 tag, and the typical use case is
structured JSON (KYC data, balances, blacklist entries), I'd recommend 64 KB per envelope as the inline limit. This accommodates:
- Small structured data (< 1 KB typical for KYC, balances)
- Medium lists (a blacklist of ~1000 addresses is ~42 KB as JSON)
- Matches typical RPC payload limits in most infrastructure
For larger data (full blacklists with 100K entries), use the persistent path with a privacyRef.
On "persistent privacy as identity": I meant that Phase 3 uses the existing IdentityRegistry contract for persistent privacy data — the
same on-chain registration + linking flow, just with privacy-specific domain hashes. But given your rename proposal, this becomes the
primary path, not a Phase 3 afterthought.
Let me revise the plan with these changes. First, let me check the current wasm_args structure to understand what's in it today.
Searched for 1 pattern, read 1 file (ctrl+o to expand)
⏺ ★ Insight ─────────────────────────────────────
The _newton reserved namespace in wasmArgs already exists as an extensible directive system. It currently handles proof_cid and
proof_type, with #[serde(flatten)] extra for forward-compatible fields. Your proposal to evolve wasm_args into a typed args struct fits
naturally into this pattern — instead of adding a new top-level field, we extend the _newton namespace with privacy and privacy_refs
keys.
But there's a nuance: wasmArgs is on-chain (it's part of the Task struct in the Solidity contract). We need to be careful — the _newton
directives are stripped before the WASM passthrough, so they don't affect the on-chain Task hash. But the raw wasmArgs bytes (including
_newton) ARE part of the BLS-signed message. This means privacy blobs in _newton would be in policyTaskData — which is exactly what we
want to avoid (keeping private data out of on-chain responses).
So we have two options:
- Extend _newton in wasmArgs — but then the encrypted blob is in the BLS-signed task data (visible on-chain)
- Keep it as a separate field on CreateTaskRequest (off-chain only) that's stripped before task creation — never enters wasmArgs or the
BLS-signed payload
Option 2 is correct for privacy. The encrypted data must NOT be in policyTaskData or wasmArgs that goes on-chain.
─────────────────────────────────────────────────
Actually, I need to reconsider your args proposal more carefully. You're suggesting renaming wasm_args to args at the RPC API level (the
CreateTaskRequest), not at the contract level. The gateway would parse the typed args struct, extract the privacy blob before creating
the on-chain task, and route each piece to the right place:
args: {
wasm: { /* existing wasm_args content / },
privacy: [ / inline HPKE envelopes / ],
privacyRefs: [ / previously uploaded data_ref_ids */ ],
}
The gateway:
- Takes args.wasm → encodes as Task.wasmArgs (on-chain)
- Takes args.privacy → passes to operators via ConsensusCommitRequest (off-chain, never on-chain)
- Takes args.privacyRefs → resolves from DB/registry, passes to operators (off-chain)
This is clean — one structured input, gateway routes each part. The on-chain Task.wasmArgs stays unchanged (only WASM args).
However, this is a breaking API change — every existing SDK consumer sends wasm_args as a hex-encoded bytes field. Renaming to args with
a new structure breaks all callers.
Suggested compromise: Keep wasm_args for backward compat, add args as the new preferred field. If both are present, args wins. Deprecate
wasm_args over time.
Let me update the design doc with all your feedback and the Phase 3 details. Before I write, let me confirm one thing about Phase 3 —
when you say "Phase 3 now," do you mean:
(a) Plan Phase 3 in detail now (Linear tickets + design), implement later
(b) Plan AND implement Phase 3 now as part of the current work
Given this is a contract-level rename (IdentityRegistry → PrivacyRegistry or at least API-level rebranding), it has significant blast
radius. My recommendation is (a) — detailed plan + tickets now, implementation as a separate tracked effort. What do you prefer?
✻ Churned for 52s
❯ 1. Extend _newton in wasmArgs — but then the encrypted blob is in the BLS-signed task data (visible on-chain) --> why is this privacy
leaking? don't we want the "encrypted" blob data or ref, etc. to be included in the onchain Task? isn't that encrypted,
privacy-preserving?
⏺ You're absolutely right — I was wrong. The encrypted blob is encrypted. Including it in the BLS-signed on-chain data doesn't leak
privacy. Only the gateway/operators with the HPKE private key can decrypt it. Anyone else seeing the on-chain wasmArgs just sees opaque
ciphertext.
★ Insight ─────────────────────────────────────
The whole point of HPKE encryption is that the ciphertext is safe to publish on-chain. The encrypted envelope contains:
- enc: ephemeral X25519 public key (not sensitive)
- ciphertext: ChaCha20-Poly1305 encrypted data (opaque without the private key)
- policy_client + chain_id: public context (already on-chain)
- recipient_pubkey: the gateway's public key (already public)
None of this leaks the plaintext. Including it in wasmArgs (and therefore in the BLS-signed Task) actually strengthens the security model
— the encrypted data is bound to the task via BLS signatures, preventing substitution attacks. If someone tries to swap the encrypted
blob, the BLS signature verification fails.
For privacyRefs (content-hash data_ref_ids), these are already keccak256 hashes — also safe on-chain.
─────────────────────────────────────────────────
So extending _newton in wasmArgs is the correct approach. This simplifies everything:
{
"wasm_key": "wasm_value",
"_newton": {
"proof_cid": "bafyproof",
"privacy": ["", ...],
"privacy_refs": ["0xabc123...", ...]
}
}
The gateway and operators already parse _newton via parse_wasm_args(). We just add privacy and privacy_refs to NewtonDirectives. The
_newton namespace is stripped before WASM execution (existing behavior), so WASM plugins don't see the privacy fields. Operators extract
the privacy data, decrypt, and inject into data.privacy Rego namespace.
Benefits:
- No new fields on CreateTaskRequest — privacy data lives in existing wasm_args
- On-chain binding via BLS signatures — encrypted blob is tamper-proof
- Backward compatible — existing wasm_args without _newton.privacy work as before
- No API rename needed — wasm_args keeps its name, _newton just gets new keys
And your args rename can happen independently later as a cosmetic API improvement.
Should I update the design doc with this approach? And to re-ask: for Phase 3 (IdentityRegistry rebranding + persistent privacy domains),
plan now + implement now, or plan now + implement separately?
❯ yes
⏺ Which "yes" — plan+implement Phase 3 now, or plan now and implement separately?
Write(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 266 lines, removed 98 lines
4
5 ## Context
6
7 -The Newton Privacy Layer currently has three separate encrypted data paths (privacy, identity, secrets) that evolved indep
-endently. This doc proposes unifying the architecture based on a key insight: persistent encrypted data and identity dat
-a are the same thing. The identity/VC flow (on-chain registry, content-hash refs, policy client linking) is the correct
-canonical implementation for all persistent encrypted data.
7 +The Newton Privacy Layer currently has three separate encrypted data paths (privacy, identity, secrets) that evolved indep
+endently. This doc proposes unifying the architecture based on two key insights:
8
9 -The remaining gap is ephemeral per-task private data — sensitive values the user provides at task submission time that
- should not be persisted. This needs a lightweight inline path, not the current DB-persistence + UUID + dual-signature app
-roach.
9 +1. Persistent encrypted data and identity data are the same thing. The identity/VC flow (on-chain registry, content-ha
+sh refs, policy client linking) is the correct canonical implementation for all persistent encrypted data. "Identity" shou
+ld be rebranded to "Privacy" as the general-purpose umbrella.
10
11 +2. Ephemeral per-task private data belongs in _newton directives inside wasmArgs. HPKE-encrypted blobs are safe to
+ include on-chain — they're opaque ciphertext. Including them in BLS-signed wasmArgs actually strengthens security by bi
+nding encrypted data to the task (preventing substitution attacks).
12 +
13 ## Current State
14
15 ### Three Encrypted Data Paths
...
28 4. No multi-client sharing — per-task binding prevents the blacklist use case (Uber + Lyft sharing a co-contributed d
ataset)
29 5. Separate Ed25519 key management — users must manage Ed25519 keys separate from their ECDSA wallet
30 6. UUID-based IDs — no content-hash dedup for identical data uploads
31 +7. Fragmented naming — "identity" and "privacy" are separate concepts in the API but architecturally the same thing
32
33 ## Proposed Architecture
34
32 -### Two Modes, Not Three
35 +### Two Modes
36
34 -Mode 1: Persistent Encrypted Data (via Identity Registry)
37 +Mode 1: Persistent Encrypted Data (Privacy Registry)
38
36 -All persistent encrypted data — identity, privacy, shared datasets — flows through the existing identity/VC path:
39 +All persistent encrypted data — KYC identity, blacklists, shared datasets, allowlists — flows through the registry path.
+The existing IdentityRegistry contract is rebranded at the API/SDK layer as the Privacy Registry. The contract itse
+lf doesn't need renaming (it's already deployed), but all new APIs, SDK functions, docs, and Rego namespaces use "privacy
+" as the umbrella term.
40
41 39 -Client → HPKE encrypt → newt_uploadIdentityEncrypted → DB + on-chain registration 40 - → IdentityRegistry.linkIdentity() → policy client access control 41 - → Operator reads on-chain link → fetches from DB → HPKE decrypts → Rego evaluation 42 +Client → HPKE encrypt → newt_uploadPrivacyData → DB + on-chain registration 43 + → PrivacyRegistry.linkPrivacyData() → policy client access control 44 + → Operator reads on-chain link → fetches from DB → HPKE decrypts → Rego 45
46
44 -The IdentityRegistry already supports:
47 +Rego namespace: data.privacy.{domain} (e.g., data.privacy.kyc, data.privacy.blacklist)
48 +
49 +The existing contract supports:
50 - Content-hash refs (deterministic, dedup-safe)
51 - On-chain registration with gateway co-signature
47 -- Multi-domain support (identity_domain as bytes32)
52 +- Multi-domain support (identity_domain as bytes32 — works for any domain hash)
53 - Policy client linking with mutual consent
54 - Revocation via unlinkIdentity
55 - PolicyClientRegistry.isRegisteredClient() enforcement
56
52 -Privacy data (blacklists, shared datasets, etc.) is just another identity domain. The identity_domain field already sup
-ports arbitrary bytes32 domain identifiers — we register new domain hashes for privacy use cases.
57 +Mode 2: Inline Ephemeral Privacy Data (via _newton directives)
58
54 -Mode 2: Inline Ephemeral Privacy Data (new)
59 +For per-task sensitive data that doesn't need persistence. Encrypted blobs travel inside wasmArgs._newton.privacy, boun
+d to the task via BLS signatures.
60
56 -For truly ephemeral per-task data that should not be persisted:
57 -
61 +json 62 +{ 63 + "wasm_key": "wasm_value", 64 + "_newton": { 65 + "proof_cid": "bafyproof", 66 + "privacy": ["<serialized HPKE SecureEnvelope>", ...], 67 + "privacy_refs": ["0xabc123...", ...] 68 + } 69 +} 70
59 -Client → HPKE encrypt → include in CreateTaskRequest (inline field)
60 - → Gateway passes encrypted blob to operators via ConsensusCommitRequest
61 - → Operator HPKE-decrypts in-memory → injects into Rego → discards
62 -71 64 -No DB write. No ref IDs. No signatures beyond the task submission itself. The encrypted blob travels with the task. 72 +The gateway and operators already parse `_newton` via `parse_wasm_args()`. Privacy entries are extracted, decrypted by op +erators, and injected into `data.privacy._inline` Rego namespace. The `_newton` namespace is stripped before WASM executi +on (existing behavior). 73 66 -### Data Flow Comparison 74 +Including encrypted blobs on-chain is safe — HPKE ciphertext is opaque without the private key. BLS-signing the `wasmArgs +` (which includes the encrypted blob) prevents substitution attacks. 75 76 +**`privacy_refs`** handles the case where encrypted data exceeds the inline size limit (64 KB per envelope). The client u +ploads via the persistent path first, gets a `data_ref_id`, and passes it in `_newton.privacy_refs`. Operators resolve fr +om DB. 77 + 78 +### Secrets (unchanged) 79 + 80 +Secrets remain a separate path — they serve a fundamentally different purpose (WASM execution context, not Rego evaluatio +n) with different auth (on-chain `getOwner()`, not registry linking). 81 + 82 +### Data Flow Summary 83 + 84
69 -PERSISTENT (identity registry path):
85 +PERSISTENT (privacy registry):
86 Upload once → register on-chain → link to policy clients → used across N tasks
87 Auth: EIP-712 at upload, on-chain link controls access
72 - Rego: data.identity.{domain}
88 + Rego: data.privacy.{domain}
89
74 -EPHEMERAL (inline path):
75 - Encrypt → submit with task → operator decrypts in-memory → discard
76 - Auth: implicit in task submission (sender chose to include it)
77 - Rego: data.data.privacy (or data.data._inline_privacy)
90 +EPHEMERAL (inline in wasmArgs._newton):
91 + HPKE encrypt → include in wasmArgs._newton.privacy → BLS-signed on-chain
92 + Auth: implicit (task submitter chose to include it, BLS binds it to task)
93 + Rego: data.privacy._inline
94 + Size limit: 64 KB per envelope
95
96 +OVERFLOW (inline ref to persistent data):
97 + Upload via persistent path → pass data_ref_id in wasmArgs._newton.privacy_refs
98 + Auth: same as persistent path (registry link required)
99 + Rego: data.privacy._inline (merged with inline data)
100 +
101 SECRETS (unchanged):
80 - Upload once → upsert by (policy_client, policy_data_address) → used across N tasks
102 + Upload once → upsert by (policy_client, policy_data_address)
103 Auth: on-chain getOwner()
82 - Not in Rego (WASM context only)
104 + WASM context only (not Rego)
105 106 107 ## Implementation Plan 108 87 -### Phase 1: Inline Ephemeral Privacy Data 109 +### Phase 1: Inline Ephemeral Privacy via `_newton` Directives 110 89 -Add support for encrypted data passed directly in the task request, with no DB persistence. 111 +Extend the existing `_newton` directive system in `wasmArgs` to carry inline encrypted privacy data. 112 91 -#### Task 1.1: Add `inline_encrypted_data` field to `CreateTaskRequest` 113 +#### Task 1.1: Extend `NewtonDirectives` with privacy fields 114 93 -**Files**: `crates/gateway/src/rpc/types/mod.rs` 115 +**Files**: `crates/core/src/common/wasm_args.rs` 116 95 -Add a new optional field to `CreateTaskRequest` and `SendTaskRequest`: 117 +Add new fields to `NewtonDirectives`: 118 119 rust
98 -/// Inline HPKE-encrypted data for ephemeral per-task privacy.
99 -/// The encrypted blob is passed directly to operators — not persisted in the database.
100 -/// Must be encrypted to the gateway/operator HPKE public key (from newt_getPrivacyPublicKey).
101 -pub inline_encrypted_data: Option<Vec>,
120 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
121 +pub struct NewtonDirectives {
122 + #[serde(skip_serializing_if = "Option::is_none")]
123 + pub proof_cid: Option,
124 + #[serde(skip_serializing_if = "Option::is_none")]
125 + pub proof_type: Option,
126 + /// Inline HPKE-encrypted privacy data (SecureEnvelope JSON strings).
127 + /// Each envelope is decrypted by operators and injected into data.privacy._inline.
128 + /// Max 64 KB per envelope.
129 + #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 + pub privacy: Vec,
131 + /// Content-hash refs to previously uploaded persistent privacy data.
132 + /// Resolved from DB by operators. Use when data exceeds inline size limit.
133 + #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 + pub privacy_refs: Vec,
135 + #[serde(default, flatten)]
136 + pub extra: serde_json::Map<String, serde_json::Value>,
137 +}
138 139 104 -Each entry is a JSON-serialized SecureEnvelope string. Multiple envelopes are supported (multiplicity). 140 +Update `parse_wasm_args()` tests for the new fields. 141 106 -#### Task 1.2: Pass inline data through `ConsensusCommitRequest` 142 +#### Task 1.2: Operator decryption of inline privacy data 143 108 -**Files**: `crates/aggregator/src/rpc_server.rs`, `crates/gateway/src/rpc/api/sync.rs` 144 +**Files**: `crates/operator/src/builder.rs`, `crates/operator/src/core.rs` 145 110 -Add `inline_encrypted_data: Option<Vec<String>>` to `ConsensusCommitRequest`. Gateway passes the encrypted blobs directly - from the task request — no DB fetch, no ref ID resolution. 146 +In the commit phase handler, after `parse_wasm_args()`: 147 +1. If `directives.privacy` is non-empty: HPKE-decrypt each envelope in-memory, validate size limit (64 KB) 148 +2. If `directives.privacy_refs` is non-empty: fetch from DB and HPKE-decrypt (reuses existing `decrypt_refs_from_db` logi +c) 149 +3. Merge all decrypted values into `data.privacy._inline` Rego namespace 150 +4. Pass via `merge_additional_data()` (extend to support `privacy._inline` key) 151 112 -#### Task 1.3: Operator decrypts inline data 152 +#### Task 1.3: Gateway passes directives through 153 114 -**Files**: `crates/operator/src/builder.rs`, `crates/operator/src/core.rs` 154 +**Files**: `crates/gateway/src/rpc/api/sync.rs` 155 116 -In the commit phase handler, if `inline_encrypted_data` is present: 117 -1. HPKE-decrypt each envelope in-memory using operator's key 118 -2. Merge decrypted values into `data.data.privacy` Rego namespace (same as current) 119 -3. No DB interaction 156 +The gateway already passes `wasmArgs` to operators via `ConsensusCommitRequest.wasm_args`. Since privacy data lives insid +e `wasmArgs._newton`, no new fields needed on `ConsensusCommitRequest`. The operator parses directives from `wasm_args` a +t evaluation time. 157 121 -#### Task 1.4: SDK support for inline privacy data 158 +For `privacy_refs`: gateway resolves DB records and validates they exist before broadcasting (fail-fast). But the actual +decryption happens operator-side. 159 160 +#### Task 1.4: Size limit enforcement 161 + 162 +**Files**: `crates/gateway/src/rpc/api/sync.rs` or `crates/core/src/common/wasm_args.rs` 163 + 164 +Validate at task creation time: 165 +- Each `_newton.privacy` entry is valid JSON (SecureEnvelope) 166 +- Each entry is <= 64 KB 167 +- Total `_newton.privacy` + `_newton.privacy_refs` count is bounded (e.g., max 20) 168 + 169 +#### Task 1.5: SDK support 170 + 171 **Files**: `newton-sdk/src/modules/privacy/index.ts`, `newton-sdk/src/types/privacy.ts` 172 125 -Add `createInlinePrivacyData()` function that: 173 +Add `createInlinePrivacyData()`: 174 1. HPKE-encrypts plaintext using gateway's public key 127 -2. Returns the serialized envelope string for inclusion in `CreateTaskRequest.inline_encrypted_data` 175 +2. Returns serialized SecureEnvelope string for inclusion in `_newton.privacy` 176 129 -No upload, no ref IDs, no signatures — just encryption. 177 +Update task submission helpers to accept optional privacy data and inject into `_newton.privacy` in `wasmArgs`. 178 131 -Update `submitEvaluationRequest` / `evaluateIntentDirect` to accept optional `inlineEncryptedData` parameter. 179 +### Phase 2: Rebrand Identity → Privacy 180 133 -### Phase 2: Deprecate Current Privacy Upload Path 181 +Unify the naming so "privacy" is the umbrella concept and identity (KYC) is one privacy domain. 182 135 -#### Task 2.1: Deprecate `newt_uploadEncryptedData` 183 +#### Task 2.1: New privacy-branded RPC endpoints (gateway) 184 137 -**Files**: `crates/gateway/src/rpc/api/privacy.rs`, `crates/gateway/src/handler/mod.rs` 185 +**Files**: `crates/gateway/src/rpc/api/`, `crates/gateway/src/rpc/types/`, `crates/gateway/src/handler/mod.rs` 186 139 -Mark endpoint as deprecated. Keep functional for backward compatibility but log deprecation warnings. Document migration -path: use inline data for ephemeral, or identity registry for persistent. 187 +Add new endpoints that wrap existing identity handlers: 188 +- `newt_uploadPrivacyData` → delegates to `upload_identity_encrypted` internally 189 +- `newt_getPrivacyData` → delegates to `get_identity_encrypted` internally 190 +- `newt_getPrivacyPublicKey` → already exists, no change 191 141 -#### Task 2.2: Deprecate SDK privacy upload functions 192 +The internal implementation is identical — just a new RPC method name. Mark old `newt_uploadIdentityEncrypted` and `newt_ +getIdentityEncrypted` as deprecated aliases. 193 143 -**Files**: `newton-sdk/src/modules/privacy/index.ts` 194 +Request/response types get privacy-branded aliases: 195 145 -Deprecate `uploadEncryptedData`, `uploadSecureEnvelope`, `signPrivacyAuthorization`. Add JSDoc `@deprecated` tags with mi -gration guidance. 196 +rust
197 +/// Alias: UploadPrivacyDataRequest wraps the existing identity upload flow.
198 +pub type UploadPrivacyDataRequest = UploadIdentityEncryptedRequest;
199 +pub type UploadPrivacyDataResponse = UploadIdentityEncryptedResponse;
200 +pub type GetPrivacyDataRequest = GetIdentityEncryptedRequest;
201 +pub type GetPrivacyDataResponse = GetIdentityEncryptedResponse;
202 + 203 147 -#### Task 2.3: Remove dual Ed25519 signature validation 204 +#### Task 2.2: Rego namespace unification 205 149 -**Files**: `crates/gateway/src/processor/privacy_auth.rs` 206 +**Files**: `crates/operator/src/core.rs`, `libs/regorus/` 207 151 -Remove `validate_privacy_signatures()` and related types. Since it was never called in the task creation path, this is de -ad code removal. 208 +Change `resolve_identity_data()` to inject into `data.privacy.{domain_name}` instead of `data.identity.{domain}`. 209 153 -#### Task 2.4: Remove `encrypted_data_refs` from `CreateTaskRequest` 210 +Domain name resolution: `keccak256("newton.privacy.kyc")` → `data.privacy.kyc`, `keccak256("newton.privacy.blacklist")` → + `data.privacy.blacklist`, etc. 211 155 -**Files**: `crates/gateway/src/rpc/types/mod.rs` 212 +Add a domain registry mapping (in `crates/core/`) that maps `bytes32` domain hashes to human-readable domain names for Re +go namespace routing. 213 157 -Remove `encrypted_data_refs`, `user_signature`, `app_signature`, `user_pubkey`, `app_pubkey` fields from `CreateTaskReque -st` and `SendTaskRequest`. Replace with `inline_encrypted_data`. 214 +Backward compat: keep `data.identity.{domain}` as a deprecated alias during transition. 215 159 -#### Task 2.5: Remove `decrypt_refs_from_db` operator path 216 +#### Task 2.3: SDK privacy module rebranding 217 161 -**Files**: `crates/operator/src/builder.rs` 218 +**Files**: `newton-sdk/src/modules/privacy/index.ts`, `newton-sdk/src/types/privacy.ts`, `newton-sdk/src/index.ts` 219 163 -Remove the DB-fetch-and-decrypt path for `encrypted_data_ref_ids`. Replace with inline decryption only. 220 +Add privacy-branded functions: 221 +- `uploadPrivacyData()` → calls `newt_uploadPrivacyData` (new endpoint) 222 +- `getPrivacyData()` → calls `newt_getPrivacyData` (new endpoint) 223 +- `registerPrivacyData()` → wraps `registerIdentityData` on-chain call 224 +- `linkPrivacyData()` → wraps `linkIdentity` on-chain call 225 +- `unlinkPrivacyData()` → wraps `unlinkIdentity` on-chain call 226 165 -#### Task 2.6: Clean up DB 227 +Deprecate identity-specific function names with `@deprecated` JSDoc tags. 228 167 -**Files**: Migration SQL 229 +Export new types: 230 +- `UploadPrivacyDataParams`, `UploadPrivacyDataResponse` 231 +- `PrivacyDomain` (enum/constants for well-known domains: `KYC`, `BLACKLIST`, `ALLOWLIST`) 232 169 -The `encrypted_data_refs` table remains for identity and secrets data types. The `privacy` data_type rows can be cleaned -up (they were ephemeral with TTL anyway). The `data_type` CHECK constraint stays — just no new `privacy` rows are created - via the old upload path. 233 +#### Task 2.4: Define well-known privacy domain identifiers 234 171 -### Phase 3: Privacy Domains via Identity Registry (Future) 235 +**Files**: `crates/core/src/common/privacy_domains.rs` (new), SDK constants 236 173 -For persistent privacy data (shared blacklists, co-contributed datasets): 237 +rust
238 +/// Well-known privacy domain identifiers.
239 +/// Each domain is keccak256 of its canonical name.
240 +pub mod privacy_domains {
241 + use alloy::primitives::{keccak256, FixedBytes};
242 +
243 + pub const KYC: FixedBytes<32> = keccak256(b"newton.privacy.kyc");
244 + pub const BLACKLIST: FixedBytes<32> = keccak256(b"newton.privacy.blacklist");
245 + pub const ALLOWLIST: FixedBytes<32> = keccak256(b"newton.privacy.allowlist");
246 +}
247 + 248 175 -#### Task 3.1: Define privacy domain identifiers 249 +SDK equivalent: 250 +typescript
251 +export const PrivacyDomain = {
252 + KYC: keccak256(toBytes("newton.privacy.kyc")),
253 + BLACKLIST: keccak256(toBytes("newton.privacy.blacklist")),
254 + ALLOWLIST: keccak256(toBytes("newton.privacy.allowlist")),
255 +} as const;
256 +```
257
177 -Register new identity_domain values for privacy use cases:
178 -- `keccak256("newton.privacy.blacklist")` — shared user blacklists
179 -- `keccak256("newton.privacy.allowlist")` — shared user allowlists
180 -- Custom domain hashes for app-specific datasets
258 +Custom domains: any `bytes32` value is valid. The well-known constants are convenience only.
259
182 -#### Task 3.2: Rego namespace routing
260 +#### Task 2.5: Newton-identity app migration
261
184 -When the operator resolves identity data for a privacy domain, inject into `data.data.privacy.{domain_name}` instead of `
-data.identity.{domain}` — or unify under a single `data.encrypted.{domain}` namespace.
262 +Files: `newton-identity/src/app/(pages)/rpc/register-user-data/page.tsx`
263
186 -This is a Rego-level change and requires updating `resolve_identity_data()` to route based on domain type.
264 +Update to use privacy-branded SDK functions:
265 +- `uploadPrivacyData()` instead of `uploadIdentityEncrypted()` (via gateway HTTP)
266 +- `registerPrivacyData()` instead of `registerIdentityData()`
267 +- Use `PrivacyDomain.KYC` constant for the domain
268
188 -#### Task 3.3: SDK helpers for privacy-as-identity
269 +#### Task 2.6: Documentation updates
270
190 -Add convenience functions that wrap the identity registration flow for privacy use cases:
191 -- `uploadPrivacyData()` → calls `uploadIdentityEncrypted` + `registerIdentityData` under the hood
192 -- `linkPrivacyData()` → calls `linkIdentity` with the appropriate privacy domain
271 +Files: `docs/PRIVACY.md`, `docs/RPC_API.md`, `docs/IDENTITY_REGISTRY_VC_ARCHITECTURE.md`, SDK docs
272
273 +- Rebrand all "identity data" references to "privacy data" (identity is one privacy domain)
274 +- Document new RPC endpoints, SDK functions, and well-known domains
275 +- Update architecture diagrams
276 +- Document inline privacy data via `_newton` directives
277 +- Document 64 KB size limit and `privacy_refs` overflow path
278 +
279 +### Phase 3: Deprecate Legacy Privacy Upload Path
280 +
281 +#### Task 3.1: Deprecate `newt_uploadEncryptedData`
282 +
283 +Files: `crates/gateway/src/rpc/api/privacy.rs`, `crates/gateway/src/handler/mod.rs`
284 +
285 +Mark endpoint as deprecated. Log warnings on each call. Document migration:
286 +- Ephemeral data → `_newton.privacy` in `wasmArgs`
287 +- Persistent data → `newt_uploadPrivacyData` + on-chain registration
288 +
289 +#### Task 3.2: Remove dual Ed25519 signature code
290 +
291 +Files: `crates/gateway/src/processor/privacy_auth.rs`
292 +
293 +Delete `validate_privacy_signatures()` and `PrivacyAuthError`/`PrivacyAuthParams` types. Dead code — never called in the
+task creation path.
294 +
295 +Remove `user_signature`, `app_signature`, `user_pubkey`, `app_pubkey` from `CreateTaskRequest`/`SendTaskRequest`.
296 +
297 +#### Task 3.3: Remove `encrypted_data_refs` from task requests
298 +
299 +Files: `crates/gateway/src/rpc/types/mod.rs`, `crates/aggregator/src/rpc_server.rs`
300 +
301 +Remove `encrypted_data_refs` from `CreateTaskRequest`/`SendTaskRequest`. Remove `encrypted_data_ref_ids` from `ConsensusC
+ommitRequest`. The `_newton.privacy_refs` field in `wasmArgs` replaces this.
302 +
303 +#### Task 3.4: Simplify operator decryption path
304 +
305 +Files: `crates/operator/src/builder.rs`
306 +
307 +Remove the old `decrypt_refs_from_db` path that reads from `request.encrypted_data_ref_ids`. Replace with:
308 +1. Parse `_newton` directives from `wasm_args`
309 +2. Decrypt `directives.privacy` (inline) and `directives.privacy_refs` (DB lookup)
310 +3. Merge into `data.privacy._inline`
311 +
312 +The persistent `resolve_identity_data()` path (for registered privacy data) remains unchanged — it reads from the on-chai
+n registry, not from task request fields.
313 +
314 +#### Task 3.5: Deprecate SDK functions
315 +
316 +Files: `newton-sdk/src/modules/privacy/index.ts`
317 +
318 +Add `@deprecated` to:
319 +- `uploadEncryptedData()` → use `uploadPrivacyData()` (persistent) or `createInlinePrivacyData()` (ephemeral)
320 +- `uploadSecureEnvelope()` → same migration
321 +- `signPrivacyAuthorization()` → remove (not needed for either path)
322 +
323 +#### Task 3.6: Clean up `privacy` data_type in DB
324 +
325 +Files: Migration SQL
326 +
327 +The `encrypted_data_refs` table keeps `identity` and `secrets` data types. Existing `privacy` rows expire via TTL. Remove
+ `privacy` from the `data_type` CHECK constraint once all rows have expired. No new `privacy` rows are created — inline d
+ata isn't persisted, and persistent data uses the registry path (stored as `identity` data_type, to be renamed in a futur
+e migration).
328 +
329 ## Migration Path for Existing Users
330
331 | Current Usage | Migration |
332 |---|---|
198 -| `uploadEncryptedData` + `encrypted_data_refs` in task | Use `inline_encrypted_data` in `CreateTaskRequest` (ephemeral)
-|
199 -| `uploadEncryptedData` for persistent reusable data | Use identity registry flow with a privacy domain (Phase 3) |
333 +| `uploadEncryptedData` + `encrypted_data_refs` in task (ephemeral) | Put encrypted envelope in `wasmArgs._newton.privacy
+` |
334 +| `uploadEncryptedData` for persistent reusable data | Use `uploadPrivacyData` + `registerPrivacyData` + `linkPrivacyData
+` |
335 | `signPrivacyAuthorization` dual signatures | Remove — not needed for either path |
201 -| SDK `uploadEncryptedData()` | Use `createInlinePrivacyData()` for ephemeral, or identity functions for persistent |
336 +| `newt_uploadIdentityEncrypted` | Use `newt_uploadPrivacyData` (alias, same handler) |
337 +| `newt_getIdentityEncrypted` | Use `newt_getPrivacyData` (alias, same handler) |
338 +| SDK `uploadEncryptedData()` | `createInlinePrivacyData()` (ephemeral) or `uploadPrivacyData()` (persistent) |
339 +| SDK `registerIdentityData()` | `registerPrivacyData()` (alias) |
340 +| SDK `linkIdentity()` | `linkPrivacyData()` (alias) |
341 +| Rego `data.identity.persona_kyc` | `data.privacy.kyc` (deprecated alias maintained) |
342
343 ## Breaking Changes
344
205 -- `CreateTaskRequest.encrypted_data_refs` removed (replaced by `inline_encrypted_data`)
345 +- `CreateTaskRequest.encrypted_data_refs` removed (replaced by `_newton.privacy` / `_newton.privacy_refs`)
346 - `CreateTaskRequest.user_signature`, `app_signature`, `user_pubkey`, `app_pubkey` removed
207 -- `newt_uploadEncryptedData` deprecated (still functional, logs warnings)
347 +- Rego namespace changes from `data.identity.{domain}` to `data.privacy.{domain}` (deprecated alias kept)
348 +- `newt_uploadEncryptedData` deprecated
349 - SDK `uploadEncryptedData()`, `signPrivacyAuthorization()` deprecated
350
210 -## Open Questions
351 +## Design Decisions
352
212 -1. Rego namespace: Should inline privacy data go to `data.data.privacy` (current) or `data.privacy` (cleaner)?
213 -2. Inline data size limit: Should we cap the size of `inline_encrypted_data` entries to prevent abuse? The current HP
-KE envelope is typically small (< 1KB), but without a limit, someone could pass large blobs.
214 -3. Phase 3 timing: Is the persistent privacy-as-identity path needed now, or can it wait until a concrete use case (e
-.g., the Uber/Lyft blacklist scenario) materializes?
215 -4. Threshold decryption: The inline path needs to support threshold HPKE when the `threshold` feature is enabled. The
- current `enc_point` field on `ConsensusCommitRequest` was designed for the DB-ref path — needs adaptation for inline dat
-a.
353 +### Why 64 KB inline limit?
354
355 +- Typical HPKE envelope for structured data (KYC, balances): < 1 KB
356 +- Medium dataset (blacklist of ~1000 addresses as JSON): ~42 KB
357 +- 64 KB accommodates most use cases while preventing abuse
358 +- Larger data (100K entry blacklists) uses `privacy_refs` (persistent path)
359 +- Aligns with typical RPC payload limits in infrastructure
360 +
361 +### Why extend `_newton` instead of adding a new `CreateTaskRequest` field?
362 +
363 +- `wasmArgs` is BLS-signed on-chain — encrypted blobs are tamper-proof
364 +- HPKE ciphertext is safe on-chain (opaque without private key)
365 +- `_newton` directive system already exists with parsing + stripping
366 +- No new fields on `CreateTaskRequest` — backward compatible
367 +- Operators already parse directives from `wasmArgs`
368 +
369 +### Why rebrand Identity → Privacy instead of the reverse?
370 +
371 +- "Privacy" is the general concept; "identity" is one specific use case (KYC)
372 +- Blacklists, allowlists, shared datasets are privacy data but not identity data
373 +- The contract stays `IdentityRegistry` (deployed, immutable name) but the API/SDK layer uses "privacy"
374 +- Future domains (financial data, medical records, etc.) fit naturally under "privacy"
375 +
376 +### Why keep secrets separate?
377 +
378 +- Secrets serve WASM execution, not Rego evaluation — different consumption point
379 +- Secrets have schema validation against on-chain PolicyData — unique requirement
380 +- Secrets auth uses `getOwner()` — simpler than registry linking
381 +- Secrets are scoped to (policy_client, policy_data_address) — different key structure
382 +
383 ## Dependencies
384
219 -- KMS-to-HPKE migration (NEWT-800) must be merged first — all encryption is HPKE-based
220 -- No contract changes needed for Phase 1-2 (all off-chain)
221 -- Phase 3 reuses existing `IdentityRegistry` contract (no new deployment)
385 +- KMS-to-HPKE migration (NEWT-800) must be merged first
386 +- No new contract deployments needed (reuses existing `IdentityRegistry`)
387 +- Phase 1 is fully off-chain (no contract changes)
388 +- Phase 2 is mostly off-chain (API + SDK + Rego) with domain constants in core
389 +- Phase 3 is cleanup (deprecation + removal)
⏺ Design doc updated at .omc/plans/privacy-data-architecture-redesign.md. Here's what changed:
Phase 1 — Inline privacy via _newton directives in wasmArgs (not a new field). Adds privacy and privacy_refs to NewtonDirectives. 64 KB
limit per envelope. Operators decrypt from directives, inject into data.privacy._inline.
Phase 2 — Rebrand Identity → Privacy at the API/SDK layer. New RPC endpoints (newt_uploadPrivacyData, newt_getPrivacyData) that alias
existing identity handlers. Rego namespace unified to data.privacy.{domain}. Well-known domain constants (KYC, BLACKLIST, ALLOWLIST).
Newton-identity app updated. Old identity endpoints kept as deprecated aliases.
Phase 3 — Deprecate legacy privacy upload path. Remove newt_uploadEncryptedData, dual Ed25519 code, encrypted_data_refs from task
requests. Clean up DB.
Key design decisions documented:
- 64 KB inline limit rationale
- Why _newton over new field (BLS binding, backward compat)
- Why Privacy over Identity as umbrella term
- Why secrets stay separate
Please review the full doc and let me know if it's ready for Linear tickets, or if you want to adjust anything.
❯ for proof_cid in _newton field in newtondirective, can that be also a vector to support multiple zkTLS proof cids to be involved during
the task?
⏺ Good catch. Let me check how proof_cid is currently used to see if multiple proofs are already supported or if it's single-valued.
Searched for 1 pattern (ctrl+o to expand)
⏺ Currently proof_cid is Option — single-valued. And looking at the usage, it's consumed as a single CID for fetching one TLS
proof.
Yes, it should be a vector for the same reason privacy is a vector — a task may involve multiple zkTLS proofs (e.g., proving bank balance
from one endpoint + proving identity from another). The NewtonDirectives struct should be consistent: all data-carrying fields support
multiplicity.
The change would be:
// Before
pub proof_cid: Option,
pub proof_type: Option,
// After
pub proof_cids: Vec,
Where ProofDirective is:
pub struct ProofDirective {
pub cid: String,
pub proof_type: Option, // "tlsn", etc.
}
Or simpler — keep them as parallel vectors:
pub proof_cids: Vec,
pub proof_types: Vec, // same length, or default to "tlsn"
But this is a separate change from the privacy redesign — it touches the zkTLS integration path (PR #380) and the challenger's proof
fetch logic. I'd recommend including it in the design doc as a "consistency improvement" task but implementing it in the zkTLS PR, not in
the privacy redesign.
Should I add it to the design doc as a noted follow-up, or do you want to include it in the current implementation scope?
❯ yes
⏺ Which one — noted follow-up, or include in current implementation scope?
❯ include it in the current implementation scope
⏺ Let me update the design doc to include the proof_cid → proof_cids vectorization as part of Phase 1 (since it's a NewtonDirectives
change).
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 77 lines, removed 7 lines
110
111 Extend the existing _newton directive system in wasmArgs to carry inline encrypted privacy data.
112
113 -#### Task 1.1: Extend NewtonDirectives with privacy fields
113 +#### Task 1.1: Extend NewtonDirectives with privacy fields + vectorize proofs
114
115 Files: crates/core/src/common/wasm_args.rs
116
117 -Add new fields to NewtonDirectives:
117 +Redesign NewtonDirectives to support multiplicity for all data-carrying fields. The current proof_cid: Option<String>
+ is single-valued, but a task may involve multiple zkTLS proofs (e.g., bank balance proof + identity proof). Make all dir
+ectives vector-based for consistency.
118
119 rust 120 +/// A zkTLS proof reference with its type identifier. 121 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 122 +pub struct ProofDirective { 123 + /// IPFS CID of the TLSNotary presentation. 124 + pub cid: String, 125 + /// Proof type identifier (e.g., "tlsn"). Defaults to "tlsn" if omitted. 126 + #[serde(default = "default_proof_type", skip_serializing_if = "is_default_proof_type")] 127 + pub proof_type: String, 128 +} 129 + 130 +fn default_proof_type() -> String { "tlsn".to_string() } 131 +fn is_default_proof_type(s: &str) -> bool { s == "tlsn" } 132 + 133 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] 134 pub struct NewtonDirectives { 122 - #[serde(skip_serializing_if = "Option::is_none")] 123 - pub proof_cid: Option<String>, 124 - #[serde(skip_serializing_if = "Option::is_none")] 125 - pub proof_type: Option<String>, 135 + /// zkTLS proof references. Replaces the old singular proof_cid/proof_type. 136 + /// Each proof is fetched from IPFS, verified, and bound into Rego. 137 + #[serde(default, skip_serializing_if = "Vec::is_empty")] 138 + pub proofs: Vec<ProofDirective>, 139 /// Inline HPKE-encrypted privacy data (SecureEnvelope JSON strings). 140 /// Each envelope is decrypted by operators and injected into data.privacy._inline. 141 /// Max 64 KB per envelope. ... 145 /// Resolved from DB by operators. Use when data exceeds inline size limit. 146 #[serde(default, skip_serializing_if = "Vec::is_empty")] 147 pub privacy_refs: Vec<String>, 148 + 149 + // --- Backward compatibility --- 150 + // Old singular fields parsed during deserialization for backward compat, 151 + // then merged into the vector fields during parse_wasm_args(). 152 + /// Deprecated: use `proofs` instead. Single proof CID for backward compat. 153 + #[serde(skip_serializing_if = "Option::is_none")] 154 + pub proof_cid: Option<String>, 155 + /// Deprecated: use `proofs` instead. Single proof type for backward compat. 156 + #[serde(skip_serializing_if = "Option::is_none")] 157 + pub proof_type: Option<String>, 158 + 159 #[serde(default, flatten)] 160 pub extra: serde_json::Map<String, serde_json::Value>, 161 } 162
163
140 -Update parse_wasm_args() tests for the new fields.
164 +After deserialization, parse_wasm_args() merges the deprecated singular fields into the vector:
165
166 +rust 167 +// Backward compat: merge singular proof_cid into proofs vec 168 +if let Some(cid) = directives.proof_cid.take() { 169 + directives.proofs.push(ProofDirective { 170 + cid, 171 + proof_type: directives.proof_type.take().unwrap_or_else(default_proof_type), 172 + }); 173 +} 174 +
175 +
176 +JSON format (new):
177 +json 178 +{ 179 + "_newton": { 180 + "proofs": [ 181 + { "cid": "bafyproof1", "proof_type": "tlsn" }, 182 + { "cid": "bafyproof2" } 183 + ], 184 + "privacy": ["<HPKE envelope JSON>"], 185 + "privacy_refs": ["0xabc123..."] 186 + } 187 +} 188 +
189 +
190 +JSON format (old, still supported):
191 +json 192 +{ 193 + "_newton": { 194 + "proof_cid": "bafyproof", 195 + "proof_type": "tlsn" 196 + } 197 +} 198 +
199 +
200 +Update parse_wasm_args() to handle the merge and update all tests.
201 +
202 +#### Task 1.1b: Update proof consumers for vector proofs
203 +
204 +Files: crates/operator/src/builder.rs, crates/challenger/src/tls_proof.rs, crates/gateway/src/rpc/api/sync.rs
205 +
206 +Update all code that reads directives.proof_cid to iterate directives.proofs instead:
207 +- Operator: fetch and verify each proof, merge all verified TLS data into Rego data.data.tls_proof (as array or keyed b
+y CID)
208 +- Challenger: verify each proof independently
209 +- Gateway: pass through in wasmArgs (already handled — proofs live in _newton)
210 +- inject_proof_cid_into_wasm_args(): update to support injecting multiple proofs (or deprecate in favor of direct _new +ton.proofs construction)
211 +
212 #### Task 1.2: Operator decryption of inline privacy data
213
214 Files: crates/operator/src/builder.rs, crates/operator/src/core.rs
⏺ Design doc updated. The NewtonDirectives redesign now includes:
- proofs: Vec replacing singular proof_cid/proof_type
- ProofDirective { cid, proof_type } struct for typed proof references
- Backward compat: old proof_cid/proof_type fields still deserialize, merged into proofs vec during parsing
- New Task 1.1b for updating all proof consumers (operator, challenger, gateway)
The full doc is at .omc/plans/privacy-data-architecture-redesign.md. Ready to review and create Linear tickets?
✻ Worked for 39s
❯ we dont really need to worry about backward compatibility because zktls nor privacy feature is yet used in production
⏺ Good point — that simplifies things significantly. Let me clean up the design doc to remove all the backward compat cruft.
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 7 lines, removed 36 lines
116
117 Redesign NewtonDirectives to support multiplicity for all data-carrying fields. The current proof_cid: Option<String>
is single-valued, but a task may involve multiple zkTLS proofs (e.g., bank balance proof + identity proof). Make all dir
ectives vector-based for consistency.
118
119 +Neither zkTLS nor the privacy upload path are in production yet, so no backward compatibility is needed. Clean break.
120 +
121 rust 122 /// A zkTLS proof reference with its type identifier. 123 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] ... 134 135 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] 136 pub struct NewtonDirectives { 135 - /// zkTLS proof references. Replaces the old singular proof_cid/proof_type. 136 - /// Each proof is fetched from IPFS, verified, and bound into Rego. 137 + /// zkTLS proof references. Each proof is fetched from IPFS, verified, 138 + /// and bound into Rego `data.data.tls_proof` as an array. 139 #[serde(default, skip_serializing_if = "Vec::is_empty")] 140 pub proofs: Vec<ProofDirective>, 141 /// Inline HPKE-encrypted privacy data (SecureEnvelope JSON strings). ... 147 /// Resolved from DB by operators. Use when data exceeds inline size limit. 148 #[serde(default, skip_serializing_if = "Vec::is_empty")] 149 pub privacy_refs: Vec<String>, 148 - 149 - // --- Backward compatibility --- 150 - // Old singular fields parsed during deserialization for backward compat, 151 - // then merged into the vector fields during parse_wasm_args(). 152 - /// Deprecated: use `proofs` instead. Single proof CID for backward compat. 153 - #[serde(skip_serializing_if = "Option::is_none")] 154 - pub proof_cid: Option<String>, 155 - /// Deprecated: use `proofs` instead. Single proof type for backward compat. 156 - #[serde(skip_serializing_if = "Option::is_none")] 157 - pub proof_type: Option<String>, 158 - 150 #[serde(default, flatten)] 151 pub extra: serde_json::Map<String, serde_json::Value>, 152 } 153
154
164 -After deserialization, parse_wasm_args() merges the deprecated singular fields into the vector:
155 +The old proof_cid: Option<String> and proof_type: Option<String> fields are removed entirely — no backward compat shi
+m needed.
156
166 -rust 167 -// Backward compat: merge singular proof_cid into proofs vec 168 -if let Some(cid) = directives.proof_cid.take() { 169 - directives.proofs.push(ProofDirective { 170 - cid, 171 - proof_type: directives.proof_type.take().unwrap_or_else(default_proof_type), 172 - }); 173 -} 174 -
175 -
176 -JSON format (new):
157 +JSON format:
158 json 159 { 160 "_newton": { ... 168 } 169
170
190 -JSON format (old, still supported):
191 -json 192 -{ 193 - "_newton": { 194 - "proof_cid": "bafyproof", 195 - "proof_type": "tlsn" 196 - } 197 -} 198 -
171 +Update parse_wasm_args() and all tests. Remove inject_proof_cid_into_wasm_args() — replace with direct _newton.proof +s construction.
172
200 -Update parse_wasm_args() to handle the merge and update all tests.
201 -
173 #### Task 1.1b: Update proof consumers for vector proofs
174
175 Files: crates/operator/src/builder.rs, crates/challenger/src/tls_proof.rs, crates/gateway/src/rpc/api/sync.rs
⏺ Now let me also clean up the Phase 2 and 3 sections to remove backward compat language since nothing is in production.
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 9 lines, removed 12 lines
225
226 Files: crates/gateway/src/rpc/api/, crates/gateway/src/rpc/types/, crates/gateway/src/handler/mod.rs
227
228 -Add new endpoints that wrap existing identity handlers:
229 -- newt_uploadPrivacyData → delegates to upload_identity_encrypted internally
230 -- newt_getPrivacyData → delegates to get_identity_encrypted internally
231 -- newt_getPrivacyPublicKey → already exists, no change
228 +Rename existing identity endpoints to privacy:
229 +- newt_uploadIdentityEncrypted → newt_uploadPrivacyData
230 +- newt_getIdentityEncrypted → newt_getPrivacyData
231 +- newt_getPrivacyPublicKey → already correct, no change
232
233 -The internal implementation is identical — just a new RPC method name. Mark old newt_uploadIdentityEncrypted and newt_ -getIdentityEncrypted as deprecated aliases.
233 +Rename request/response types directly (no aliases — nothing in production):
234
235 -Request/response types get privacy-branded aliases:
236 -
235 rust 238 -/// Alias: UploadPrivacyDataRequest wraps the existing identity upload flow. 239 -pub type UploadPrivacyDataRequest = UploadIdentityEncryptedRequest; 240 -pub type UploadPrivacyDataResponse = UploadIdentityEncryptedResponse; 241 -pub type GetPrivacyDataRequest = GetIdentityEncryptedRequest; 242 -pub type GetPrivacyDataResponse = GetIdentityEncryptedResponse; 236 +pub struct UploadPrivacyDataRequest { ... } // was UploadIdentityEncryptedRequest 237 +pub struct UploadPrivacyDataResponse { ... } // was UploadIdentityEncryptedResponse 238 +pub struct GetPrivacyDataRequest { ... } // was GetIdentityEncryptedRequest 239 +pub struct GetPrivacyDataResponse { ... } // was GetIdentityEncryptedResponse 240
241
242 #### Task 2.2: Rego namespace unification
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 1 line, removed 3 lines
247
248 Domain name resolution: keccak256("newton.privacy.kyc") → data.privacy.kyc, keccak256("newton.privacy.blacklist") →
data.privacy.blacklist, etc.
249
250 -Add a domain registry mapping (in crates/core/) that maps bytes32 domain hashes to human-readable domain names for Re
-go namespace routing.
250 +Add a domain registry mapping (in crates/core/) that maps bytes32 domain hashes to human-readable domain names for Re
+go namespace routing. No backward compat needed — data.identity.* is not in production.
251
252 -Backward compat: keep data.identity.{domain} as a deprecated alias during transition.
253 -
252 #### Task 2.3: SDK privacy module rebranding
253
254 Files: newton-sdk/src/modules/privacy/index.ts, newton-sdk/src/types/privacy.ts, newton-sdk/src/index.ts
⏺ Now clean up Phase 2.3 SDK and Phase 3 deprecation sections.
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 12 lines, removed 8 lines
253
254 Files: newton-sdk/src/modules/privacy/index.ts, newton-sdk/src/types/privacy.ts, newton-sdk/src/index.ts
255
256 -Add privacy-branded functions:
257 -- uploadPrivacyData() → calls newt_uploadPrivacyData (new endpoint)
258 -- getPrivacyData() → calls newt_getPrivacyData (new endpoint)
259 -- registerPrivacyData() → wraps registerIdentityData on-chain call
260 -- linkPrivacyData() → wraps linkIdentity on-chain call
261 -- unlinkPrivacyData() → wraps unlinkIdentity on-chain call
256 +Rename identity functions to privacy (no deprecation aliases — identity module not in production):
257 +- uploadEncryptedData() → remove (replaced by inline + persistent paths)
258 +- uploadSecureEnvelope() → remove
259 +- signPrivacyAuthorization() → remove
260 +- registerIdentityData() → registerPrivacyData()
261 +- linkIdentity() / linkIdentityAsSigner() / etc. → linkPrivacyData() / linkPrivacyDataAsSigner() / etc.
262 +- unlinkIdentityAsSigner() / etc. → unlinkPrivacyDataAsSigner() / etc.
263
263 -Deprecate identity-specific function names with @deprecated JSDoc tags.
264 +Add new functions:
265 +- uploadPrivacyData() → calls newt_uploadPrivacyData
266 +- getPrivacyData() → calls newt_getPrivacyData
267 +- createInlinePrivacyData() → HPKE encrypts for _newton.privacy inclusion
268
269 Export new types:
270 - UploadPrivacyDataParams, UploadPrivacyDataResponse
267 -- PrivacyDomain (enum/constants for well-known domains: KYC, BLACKLIST, ALLOWLIST)
271 +- PrivacyDomain (constants for well-known domains: KYC, BLACKLIST, ALLOWLIST)
272
273 #### Task 2.4: Define well-known privacy domain identifiers
274
⏺ Now clean up Phase 3 — since nothing is in production, it's not "deprecation" but "removal."
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 21 lines, removed 17 lines
316 - Document inline privacy data via _newton directives
317 - Document 64 KB size limit and privacy_refs overflow path
318
319 -### Phase 3: Deprecate Legacy Privacy Upload Path
319 +### Phase 3: Remove Legacy Privacy Upload Path
320
321 -#### Task 3.1: Deprecate newt_uploadEncryptedData
321 +Nothing is in production — clean removal, no deprecation period.
322
323 -Files: crates/gateway/src/rpc/api/privacy.rs, crates/gateway/src/handler/mod.rs
323 +#### Task 3.1: Remove newt_uploadEncryptedData endpoint
324
325 -Mark endpoint as deprecated. Log warnings on each call. Document migration:
325 +Files: crates/gateway/src/rpc/api/privacy.rs, crates/gateway/src/handler/mod.rs, crates/gateway/src/rpc/types/pr +ivacy.rs
326 +
327 +Delete the endpoint, handler, and request/response types entirely. Replaced by:
328 - Ephemeral data → _newton.privacy in wasmArgs
327 -- Persistent data → newt_uploadPrivacyData + on-chain registration
329 +- Persistent data → newt_uploadPrivacyData (renamed identity endpoint)
330
331 #### Task 3.2: Remove dual Ed25519 signature code
332
333 Files: crates/gateway/src/processor/privacy_auth.rs
334
333 -Delete validate_privacy_signatures() and PrivacyAuthError/PrivacyAuthParams types. Dead code — never called in the
-task creation path.
335 +Delete validate_privacy_signatures(), PrivacyAuthError, PrivacyAuthParams, and the entire module. Dead code — never
+ called.
336
337 Remove user_signature, app_signature, user_pubkey, app_pubkey from CreateTaskRequest/SendTaskRequest.
338
...
340
341 Files: crates/gateway/src/rpc/types/mod.rs, crates/aggregator/src/rpc_server.rs
342
341 -Remove encrypted_data_refs from CreateTaskRequest/SendTaskRequest. Remove encrypted_data_ref_ids from ConsensusC -ommitRequest. The _newton.privacy_refs field in wasmArgs replaces this.
343 +Remove encrypted_data_refs from CreateTaskRequest/SendTaskRequest. Remove encrypted_data_ref_ids from ConsensusC +ommitRequest. The _newton.privacy and _newton.privacy_refs fields in wasmArgs replace this.
344
343 -#### Task 3.4: Simplify operator decryption path
345 +#### Task 3.4: Remove old operator decryption path
346
347 Files: crates/operator/src/builder.rs
348
347 -Remove the old decrypt_refs_from_db path that reads from request.encrypted_data_ref_ids. Replace with:
349 +Remove decrypt_refs_from_db() and the PrivacyResources struct that reads from request.encrypted_data_ref_ids. Repla
+ce with:
350 1. Parse _newton directives from wasm_args
351 2. Decrypt directives.privacy (inline) and directives.privacy_refs (DB lookup)
352 3. Merge into data.privacy._inline
353
352 -The persistent resolve_identity_data() path (for registered privacy data) remains unchanged — it reads from the on-chai
-n registry, not from task request fields.
354 +The persistent resolve_identity_data() path (renamed to resolve_privacy_data()) remains — it reads from the on-chain
+registry, not from task request fields.
355
354 -#### Task 3.5: Deprecate SDK functions
356 +#### Task 3.5: Remove legacy SDK functions
357
358 Files: newton-sdk/src/modules/privacy/index.ts
359
358 -Add @deprecated to:
359 -- uploadEncryptedData() → use uploadPrivacyData() (persistent) or createInlinePrivacyData() (ephemeral)
360 -- uploadSecureEnvelope() → same migration
361 -- signPrivacyAuthorization() → remove (not needed for either path)
360 +Delete entirely (not in production):
361 +- uploadEncryptedData()
362 +- uploadSecureEnvelope()
363 +- signPrivacyAuthorization()
364
363 -#### Task 3.6: Clean up privacy data_type in DB
365 +These are replaced by createInlinePrivacyData() (ephemeral) and uploadPrivacyData() (persistent).
366
367 +#### Task 3.6: Clean up DB privacy data_type
368 +
369 Files: Migration SQL
370
367 -The encrypted_data_refs table keeps identity and secrets data types. Existing privacy rows expire via TTL. Remove
- privacy from the data_type CHECK constraint once all rows have expired. No new privacy rows are created — inline d
-ata isn't persisted, and persistent data uses the registry path (stored as identity data_type, to be renamed in a futur
-e migration).
371 +Remove privacy from the data_type CHECK constraint. Rename identity data_type to privacy (or keep as-is and treat
+ identity as a legacy alias — the API layer handles the naming). Drop any remaining privacy rows (ephemeral, all expi
+red by TTL).
372
373 ## Migration Path for Existing Users
374
⏺ Now update the migration table and breaking changes section too.
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 60 lines, removed 17 lines
370
371 Remove privacy from the data_type CHECK constraint. Rename identity data_type to privacy (or keep as-is and treat
identity as a legacy alias — the API layer handles the naming). Drop any remaining privacy rows (ephemeral, all expi
red by TTL).
372
373 -## Migration Path for Existing Users
373 +## API Surface (After All Phases)
374
375 -| Current Usage | Migration |
376 -|---|---|
377 -| uploadEncryptedData + encrypted_data_refs in task (ephemeral) | Put encrypted envelope in wasmArgs._newton.privacy - |
378 -| uploadEncryptedData for persistent reusable data | Use uploadPrivacyData + registerPrivacyData + linkPrivacyData - |
379 -| signPrivacyAuthorization dual signatures | Remove — not needed for either path |
380 -| newt_uploadIdentityEncrypted | Use newt_uploadPrivacyData (alias, same handler) |
381 -| newt_getIdentityEncrypted | Use newt_getPrivacyData (alias, same handler) |
382 -| SDK uploadEncryptedData() | createInlinePrivacyData() (ephemeral) or uploadPrivacyData() (persistent) |
383 -| SDK registerIdentityData() | registerPrivacyData() (alias) |
384 -| SDK linkIdentity() | linkPrivacyData() (alias) |
385 -| Rego data.identity.persona_kyc | data.privacy.kyc (deprecated alias maintained) |
375 +### RPC Endpoints
376
377 +| Endpoint | Purpose | Auth |
378 +|----------|---------|------|
379 +| newt_uploadPrivacyData | Upload persistent encrypted data (any domain) | EIP-712 + gateway co-sign |
380 +| newt_getPrivacyData | Retrieve encrypted data by ref ID | API key (RpcRead) |
381 +| newt_getPrivacyPublicKey | Get gateway's HPKE public key | None |
382 +| newt_storeEncryptedSecrets | Upload WASM secrets (unchanged) | On-chain getOwner() |
383 +| newt_createTask | Submit task (inline privacy via _newton) | API key (RpcWrite) |
384 +
385 +### Removed Endpoints
386 +
387 +| Endpoint | Replacement |
388 +|----------|-------------|
389 +| newt_uploadEncryptedData | _newton.privacy (ephemeral) or newt_uploadPrivacyData (persistent) |
390 +| newt_uploadIdentityEncrypted | newt_uploadPrivacyData (renamed) |
391 +| newt_getIdentityEncrypted | newt_getPrivacyData (renamed) |
392 +
393 +### SDK Functions
394 +
395 +| Function | Purpose |
396 +|----------|---------|
397 +| createInlinePrivacyData() | HPKE encrypt for _newton.privacy inclusion |
398 +| uploadPrivacyData() | Upload + register persistent privacy data |
399 +| getPrivacyData() | Retrieve encrypted data by ref ID |
400 +| registerPrivacyData() | On-chain registration (wraps IdentityRegistry) |
401 +| linkPrivacyData() / variants | On-chain policy client linking |
402 +| unlinkPrivacyData() / variants | On-chain unlinking |
403 +| createSecureEnvelope() | Low-level HPKE encryption (unchanged) |
404 +| getPrivacyPublicKey() | Fetch gateway HPKE key (unchanged) |
405 +| storeEncryptedSecrets() | WASM secrets upload (unchanged) |
406 +
407 +### Removed SDK Functions
408 +
409 +| Function | Replacement |
410 +|----------|-------------|
411 +| uploadEncryptedData() | createInlinePrivacyData() or uploadPrivacyData() |
412 +| uploadSecureEnvelope() | createInlinePrivacyData() or uploadPrivacyData() |
413 +| signPrivacyAuthorization() | Removed (not needed) |
414 +| registerIdentityData() | registerPrivacyData() |
415 +| linkIdentity() / variants | linkPrivacyData() / variants |
416 +
417 +### Rego Namespaces
418 +
419 +| Namespace | Source |
420 +|-----------|--------|
421 +| data.privacy.{domain} | Persistent data via registry (e.g., data.privacy.kyc, data.privacy.blacklist) |
422 +| data.privacy._inline | Ephemeral inline data from _newton.privacy / _newton.privacy_refs |
423 +| data.data.tls_proof | zkTLS proofs from _newton.proofs (array) |
424 +
425 ## Breaking Changes
426
389 -- CreateTaskRequest.encrypted_data_refs removed (replaced by _newton.privacy / _newton.privacy_refs)
390 -- CreateTaskRequest.user_signature, app_signature, user_pubkey, app_pubkey removed
391 -- Rego namespace changes from data.identity.{domain} to data.privacy.{domain} (deprecated alias kept)
392 -- newt_uploadEncryptedData deprecated
393 -- SDK uploadEncryptedData(), signPrivacyAuthorization() deprecated
427 +None of the affected features are in production. Clean break:
428
429 +- newt_uploadEncryptedData removed
430 +- newt_uploadIdentityEncrypted → renamed to newt_uploadPrivacyData
431 +- newt_getIdentityEncrypted → renamed to newt_getPrivacyData
432 +- CreateTaskRequest.encrypted_data_refs, user_signature, app_signature, user_pubkey, app_pubkey removed
433 +- ConsensusCommitRequest.encrypted_data_ref_ids removed
434 +- NewtonDirectives.proof_cid, proof_type → replaced by proofs: Vec<ProofDirective>
435 +- Rego data.identity.* → data.privacy.*
436 +- SDK identity functions renamed to privacy
437 +
438 ## Design Decisions
439
440 ### Why 64 KB inline limit?
⏺ Design doc is clean. Let me do a final read of the full doc to make sure it's coherent end-to-end.
Read 1 file (ctrl+o to expand)
⏺ The doc is coherent. One small inconsistency to fix — the Mode 2 JSON example on line 65 still shows the old proof_cid format instead of
the new proofs array.
⏺ Update(.omc/plans/privacy-data-architecture-redesign.md)
⎿ Added 1 line, removed 1 line
62 {
63 "wasm_key": "wasm_value",
64 "_newton": {
65 - "proof_cid": "bafyproof",
65 + "proofs": [{ "cid": "bafyproof", "proof_type": "tlsn" }],
66 "privacy": ["", ...],
67 "privacy_refs": ["0xabc123...", ...]
68 }
⏺ Design doc is clean and consistent. Ready for your review.
Summary of the plan:
┌───────┬──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Phase │ Scope │ Tasks │
├───────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Phase │ Inline ephemeral │ 1.1: Redesign NewtonDirectives (privacy + vectorized proofs), 1.1b: Update proof consumers, 1.2: │
│ 1 │ privacy via _newton │ Operator inline decryption, 1.3: Gateway passthrough, 1.4: Size limits, 1.5: SDK │
│ │ │ createInlinePrivacyData() │
├───────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Phase │ Rebrand Identity → │ 2.1: Rename RPC endpoints, 2.2: Rego namespace data.privacy.*, 2.3: SDK rename, 2.4: Well-known │
│ 2 │ Privacy │ domain constants, 2.5: Newton-identity app update, 2.6: Docs │
├───────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Phase │ Remove legacy │ 3.1: Remove newt_uploadEncryptedData, 3.2: Remove dual Ed25519 code, 3.3: Remove │
│ 3 │ privacy upload │ encrypted_data_refs from requests, 3.4: Remove old operator decrypt path, 3.5: Remove legacy SDK │
│ │ │ functions, 3.6: DB cleanup │
└───────┴──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────┘
17 tasks total across 3 phases. All phases planned for immediate implementation. No backward compat needed.
Want me to create the Linear tickets now?
⏺ All 17 Linear tickets created with dependency chains. Here's the full map:
Phase 1 — Inline Ephemeral Privacy (High priority)
┌──────────┬────────────────────────────────────────────────────────┬────────────┐
│ Ticket │ Title │ Blocked by │
├──────────┼────────────────────────────────────────────────────────┼────────────┤
│ NEWT-899 │ Redesign NewtonDirectives: privacy + vectorized proofs │ — │
├──────────┼────────────────────────────────────────────────────────┼────────────┤
│ NEWT-900 │ Update proof consumers for vector proofs │ NEWT-899 │
├──────────┼────────────────────────────────────────────────────────┼────────────┤
│ NEWT-901 │ Operator decryption of inline privacy data │ NEWT-899 │
├──────────┼────────────────────────────────────────────────────────┼────────────┤
│ NEWT-902 │ Gateway validation for inline privacy data │ NEWT-899 │
├──────────┼────────────────────────────────────────────────────────┼────────────┤
│ NEWT-903 │ SDK: createInlinePrivacyData() │ NEWT-899 │
└──────────┴────────────────────────────────────────────────────────┴────────────┘
Phase 2 — Rebrand Identity → Privacy (High/Medium)
┌──────────┬───────────────────────────────────────────────┬────────────────────┐
│ Ticket │ Title │ Blocked by │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-904 │ Rename identity RPC endpoints to privacy │ NEWT-901, NEWT-902 │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-905 │ Unify Rego namespace to data.privacy.{domain} │ NEWT-904, NEWT-907 │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-906 │ SDK: rebrand identity module to privacy │ NEWT-904, NEWT-903 │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-907 │ Define well-known privacy domain identifiers │ — │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-908 │ Newton-identity app: privacy-branded SDK │ NEWT-906 │
├──────────┼───────────────────────────────────────────────┼────────────────────┤
│ NEWT-914 │ Documentation: privacy architecture redesign │ NEWT-905, NEWT-906 │
└──────────┴───────────────────────────────────────────────┴────────────────────┘
Phase 3 — Remove Legacy (Medium/Low)
┌──────────┬────────────────────────────────────────────┬────────────────────┐
│ Ticket │ Title │ Blocked by │
├──────────┼────────────────────────────────────────────┼────────────────────┤
│ NEWT-909 │ Remove newt_uploadEncryptedData endpoint │ NEWT-901, NEWT-904 │
├──────────┼────────────────────────────────────────────┼────────────────────┤
│ NEWT-910 │ Remove dual Ed25519 + encrypted_data_refs │ NEWT-909 │
├──────────┼────────────────────────────────────────────┼────────────────────┤
│ NEWT-911 │ Remove old operator decrypt_refs_from_db │ NEWT-901, NEWT-910 │
├──────────┼────────────────────────────────────────────┼────────────────────┤
│ NEWT-912 │ Remove legacy SDK privacy upload functions │ NEWT-906 │
├──────────┼────────────────────────────────────────────┼────────────────────┤
│ NEWT-913 │ DB cleanup: rename identity → privacy │ NEWT-911 │
└──────────┴────────────────────────────────────────────┴────────────────────┘
Previously created:
┌──────────┬────────────────────────────────────────────────┐
│ Ticket │ Title │
├──────────┼────────────────────────────────────────────────┤
│ NEWT-898 │ Client-side secret retrieval (dual encryption) │
└──────────┴────────────────────────────────────────────────┘
The critical path is: NEWT-899 → NEWT-901/902 → NEWT-904 → NEWT-905/906 → NEWT-909/910/911. Ready to start implementation?
★ Insight ─────────────────────────────────────
The privacy data ownership model determines the entire trust chain: who encrypts, who uploads, who links to policy clients, and who pays
gas for on-chain registration. Getting this wrong means either (a) users can't use the system without trusting a centralized party, or
(b) compliance providers can't enforce data integrity.
─────────────────────────────────────────────────
Let me think through the actors, data types, and flows:
Actors in the Privacy Layer
┌──────────────────┬──────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────┐
│ Actor │ Role │ Examples │
├──────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
│ Privacy Data │ Entity that possesses authoritative data and │ KYC provider, compliance firm, DAO governance contract, │
│ Owner │ uploads it │ token issuer │
├──────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
│ Policy Client │ On-chain contract that defines evaluation rules │ DeFi protocol, bridge, DEX │
├──────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
│ End User │ Wallet address being evaluated against privacy │ Trader, borrower, sender │
│ │ data │ │
├──────────────────┼──────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────┤
│ Operator │ Decrypts + evaluates during task execution │ Newton operator node │
└──────────────────┴──────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────┘
Privacy Data Domains & Ownership
┌───────────────────────────────┬───────────────────────────────────┬──────────────────────────────┬───────────────────────────────┐
│ Domain │ Data Owner │ What's Uploaded │ Example Use Case │
├───────────────────────────────┼───────────────────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ │ KYC provider (e.g., Chainalysis, │ User's KYC status, risk │ DeFi protocol requires │
│ KYC (newton.privacy.kyc) │ Jumio, on-chain attestation │ tier, jurisdiction │ KYC-verified users for >$10K │
│ │ issuer) │ │ swaps │
├───────────────────────────────┼───────────────────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ Blacklist │ Compliance provider, OFAC oracle, │ Set of sanctioned/flagged │ Bridge blocks transfers from │
│ (newton.privacy.blacklist) │ chain analytics firm │ addresses │ OFAC-listed wallets │
├───────────────────────────────┼───────────────────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ Allowlist │ Protocol owner, DAO multisig, │ Set of pre-approved │ Token launch restricts early │
│ (newton.privacy.allowlist) │ token issuer │ addresses for gated access │ access to allowlisted wallets │
├───────────────────────────────┼───────────────────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ │ Credit scoring protocol (e.g., │ User's on-chain credit │ Lending protocol adjusts │
│ Credit Score (future) │ Spectral, Cred) │ score, history │ collateral ratio based on │
│ │ │ │ score │
├───────────────────────────────┼───────────────────────────────────┼──────────────────────────────┼───────────────────────────────┤
│ │ │ Accreditation status, expiry │ Security token offering │
│ Accredited Investor (future) │ Regulated attestation provider │ date │ restricted to accredited │
│ │ │ │ investors │
└───────────────────────────────┴───────────────────────────────────┴──────────────────────────────┴───────────────────────────────┘
E2E Flows
Flow 1: Blacklist (Compliance Provider Uploads)
- Chainalysis (data owner) maintains a sanctioned address list
- Chainalysis encrypts the list → HPKE SecureEnvelope using gateway's public key
- Two paths:
a. PERSISTENT: Upload via newt_uploadPrivacyData → gets data_ref_id
→ Register on-chain: PrivacyRegistry.registerPrivacyData(data_ref_id, gateway_sig)
→ Link to policy client: PrivacyRegistry.linkPrivacyData(policyClient, user)
b. EPHEMERAL: Embed in wasmArgs._newton.privacy per-task (for one-off checks) - Bridge (policy client) creates a task: "Can wallet 0xABC transfer 10 ETH?"
- Operator decrypts blacklist locally, injects into Rego data.privacy.blacklist
- Rego policy:
deny { input.intent.sender in data.privacy.blacklist.addresses } - Operator signs result → BLS aggregation → on-chain attestation
Key: Chainalysis is the data owner. They update the list periodically. The bridge (policy client) never sees the raw list — only the
allow/deny result.
Flow 2: KYC (KYC Provider Uploads Per-User)
- Jumio (data owner) verifies user 0xABC's identity off-chain
- Jumio encrypts KYC status → SecureEnvelope
- PERSISTENT path: Upload + register + link to the DeFi protocol's policy client
- data_ref_id is content-addressed (keccak256 of ciphertext)
- Linked to user address on PrivacyRegistry
- DeFi protocol submits task: "Can 0xABC swap 50K USDC?"
- Operator fetches encrypted KYC ref from DB, decrypts locally
- Rego:
allow { data.privacy.kyc.status == "verified"; data.privacy.kyc.tier >= 2 } - Result: allow (user is KYC tier 2+)
Key: Jumio owns the data, but it's linked to a specific user AND policy client. The user consented to Jumio sharing their status with
this specific DeFi protocol.
Flow 3: Allowlist (Protocol Owner Uploads)
- DAO multisig (data owner) curates an allowlist for token launch
- DAO encrypts allowlist → SecureEnvelope
- PERSISTENT: Upload, register, link to their own policy client
- User 0xDEF tries to mint: task submitted
- Operator decrypts, checks:
allow { input.intent.sender in data.privacy.allowlist.addresses } - Result: allow/deny based on list membership
Key: The protocol itself is the data owner. They manage their own list. Simplest ownership model.
Flow 4: Ephemeral Per-Task Privacy (User Self-Attests)
- User 0xABC wants to prove something for THIS specific task only
- User encrypts their data → base64 ciphertext
- Embeds in wasmArgs._newton.privacy[]: ["base64-envelope-1"]
- Task submitted → operators decrypt inline → evaluate → discard
- No on-chain registration, no persistent storage
Key: The USER is the data owner. Data lives only for the task duration. Good for self-attestation or one-off proofs.
The Ownership Question: Who Uploads?
┌──────────────────┬──────────────────────┬────────────────────────┬──────────────────────────┬─────────────────────────────────────┐
│ Pattern │ Data Owner │ Uploads To │ Registers On-Chain │ Links To Policy Client │
├──────────────────┼──────────────────────┼────────────────────────┼──────────────────────────┼─────────────────────────────────────┤
│ Provider-managed │ Compliance/KYC │ newt_uploadPrivacyData │ Provider calls │ Provider or user calls │
│ │ provider │ │ registerPrivacyData │ linkPrivacyData │
├──────────────────┼──────────────────────┼────────────────────────┼──────────────────────────┼─────────────────────────────────────┤
│ Protocol-managed │ Protocol/DAO owner │ newt_uploadPrivacyData │ Protocol calls │ Protocol links to own policy client │
│ │ │ │ registerPrivacyData │ │
├──────────────────┼──────────────────────┼────────────────────────┼──────────────────────────┼─────────────────────────────────────┤
│ User │ End user │ Inline _newton.privacy │ None (ephemeral) │ Implicit (per-task) │
│ self-service │ │ │ │ │
├──────────────────┼──────────────────────┼────────────────────────┼──────────────────────────┼─────────────────────────────────────┤
│ │ Provider uploads, │ │ │ User signs linkPrivacyData to │
│ Hybrid │ user authorizes │ newt_uploadPrivacyData │ Provider registers │ authorize sharing with specific │
│ │ │ │ │ policy client │
└──────────────────┴──────────────────────┴────────────────────────┴──────────────────────────┴─────────────────────────────────────┘
The hybrid pattern is the most production-realistic for KYC/compliance:
- Provider owns the data quality/integrity
- User controls which policy clients can access their data (privacy consent)
- linkPrivacyData requires the user's signature (or the privacy owner's, depending on deployment)
Open Design Questions
- Data freshness: How does a compliance provider update a blacklist? Re-upload with new data_ref_id? Or mutable reference?
- Current: immutable content-addressed refs. Update = new upload + new data_ref_id + re-link
- Could add: updatePrivacyData(old_ref_id, new_ref_id) for atomic swap - Bulk vs per-user: Blacklists are bulk (one list for all users). KYC is per-user. Should they use different storage patterns?
- Bulk: one data_ref_id linked to policy client, no user dimension
- Per-user: data_ref_id per user, linked to (policy_client, user) pair - Data expiry: KYC status expires. Who enforces re-verification?
- Could embed expires_at in the SecureEnvelope metadata
- Rego policy checks: deny { data.privacy.kyc.expires_at < time.now_ns() } - Cross-policy-client sharing: Can user share one KYC attestation across multiple DeFi protocols?
- Current: linkPrivacyData is per (policy_client, user) — user must link to each
- Could add: batch link or "share with all clients owned by address X" - Who pays gas?: registerPrivacyData and linkPrivacyData are on-chain calls
- Provider-managed: provider pays (they have business relationship)
- User self-service: user pays (they want access)
- Meta-transactions / gasless: gateway relays with user signature
Want me to dig deeper into any of these patterns or start spec'ing the Rego policy interfaces for each domain?
✻ Cooked for 55s
❯ but we have dual signature verification process during linking the private data ref to the policy client for the user application wallet
for each of one single user's private data, right? And during policy evaluation, we require user's signature to consent the usage of
private data of the user during policy evaluation? So where we are getting at here is user "privacy" data vs policy-client(s)
"confidential" data. Do a direct comparison analysis between the two flows end to end if review if both cases are clearly and correctly
supported
⏺ Great distinction. Let me trace both flows through the actual codebase to verify they're correctly supported.
⏺ oh-my-claudecode:architect(Analyze privacy vs confidential flows)
⎿ Done (29 tool uses · 148.8k tokens · 2m 41s)
(ctrl+o to expand)
⏺ Excellent analysis. Here's the structured comparison:
User Privacy Data vs Policy Client Confidential Data
┌────────────────┬──────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────┐
│ Aspect │ User Privacy Data (KYC) │ Policy Client Confidential Data │
│ │ │ (Blacklist/Allowlist) │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Data owner │ Individual user (EOA) │ Compliance provider / protocol owner │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Data scope │ Per-user, per-domain │ Per-policy-client (shared across all users) │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Upload │ newt_uploadPrivacyData + user EIP-712 sig │ Not implemented │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ On-chain │ registerPrivacyData (gateway co-signs) │ Not implemented │
│ registration │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Linking │ 4 variants with dual-sig (linkPrivacyData*) │ N/A — data applies to all users, no per-user │
│ │ │ link needed │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Consent at │ Owner and/or user EIP-712 signatures │ Only client owner signature needed │
│ link time │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Consent at │ Implicit — intent signature proves user identity, operator looks │ N/A — no per-user consent needed │
│ eval time │ up policyClientLinks[client][intentSigner][domain] │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Evaluation │ policyClientLinks[client][intentSigner][domain] → owner → │ No path exists — lookup requires intentSigner │
│ lookup │ data_ref_id │ key, which doesn't apply │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Domain │ │ BLACKLIST/ALLOWLIST constants exist in │
│ dispatch │ Kyc variant in PrivacyDomain enum │ privacy_domains.rs but not connected to │
│ │ │ PrivacyDomain::from_bytes32 │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Revocation │ unlinkPrivacyDataAsSigner / unlinkPrivacyDataAsUser │ Not implemented │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Ephemeral │ _newton.privacy inline per-task │ Same, but impractical for large datasets │
│ fallback │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Status │ Fully implemented │ Constants defined; all paths missing │
└────────────────┴──────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────┘
The Core Architectural Gap
★ Insight ─────────────────────────────────────
The PrivacyRegistry contract and fetch_privacy_data Rust function are built around a per-user linkage model:
policyClientLinks[policyClient][clientUser][privacyDomain]. This works perfectly for user privacy data where clientUser = the intent
signer.
For policy client confidential data (blacklists), there is no individual clientUser — the data applies to ALL users of that policy
client. The current lookup would return Address::ZERO and error because no per-user link exists.
Additionally, PrivacyDomain::from_bytes32() in chainio/src/privacy_data.rs:85-91 only recognizes keccak256(b"kyc"). The
BLACKLIST/ALLOWLIST constants in privacy_domains.rs use newton.privacy.blacklist naming — these are not connected to the dispatch enum.
─────────────────────────────────────────────────
What's Correctly Supported (User Privacy Flow)
The dual-signature linking is well-designed with 4 variants:
┌────────────────────────────────┬──────────────────────┬──────────────────────────┐
│ Variant │ Caller │ Sigs Required │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsSignerAndUser │ Same address is both │ None (msg.sender) │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsSigner │ Privacy owner │ User's EIP-712 sig │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsUser │ User │ Owner's EIP-712 sig │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyData │ Any 3rd party │ Both owner AND user sigs │
└────────────────────────────────┴──────────────────────┴──────────────────────────┘
Consent at evaluation time is implicit: the operator recovers the intent signer from the intent EIP-712 signature, then uses that address
as the clientUser key in the on-chain lookup. If the link exists, the data was previously consented. No per-task consent directive
exists.
What's Missing (Policy Client Confidential Flow)
Three things need to happen to support blacklists/allowlists:
- Contract: Add a policy-client-level data path — either a new policyClientData[policyClient][domain] mapping or reuse policyClientLinks
with a sentinel address like address(1). Only the policy client owner would need to sign. - Rust resolution: fetch_privacy_data needs a fallback — if per-user policyClientLinks returns zero, check the policy-client-level
mapping. - Domain dispatch: Connect BLACKLIST/ALLOWLIST constants to PrivacyDomain enum variants and add deserialize_privacy_data arms.
Want me to spec out the contract changes and Rust resolution path for this, or create Linear tickets for the work?
⏺ Ran 4 stop hooks (ctrl+o to expand)
⎿ Stop hook error: [AUTOPILOT - Phase: unknown] Autopilot not complete. Continue working. When all phases are complete, run
/oh-my-claudecode:cancel to cleanly exit and clean up this session's autopilot state files. If cancel fails, retry with
/oh-my-claudecode:cancel --force.
⏺ Skill(oh-my-claudecode:cancel)
⎿ Successfully loaded skill
⏺ plugin:oh-my-claudecode:t - state_list_active (MCP)
⏺ plugin:oh-my-claudecode:t - state_clear (MCP)(mode: "autopilot", session_id: "5ef86c15-9c62-40c4-94a2-68d2f20e9af2")
⏺ Autopilot state cleared. Session complete.
Summary of what we covered today:
- lessons.md optimization: 40,817 → 32,472 chars (-20%). Removed 11 architectural-advice entries, merged 1 duplicate, condensed 7
verbose entries. All sacred failure-mode entries preserved. - channel.rs analysis: Confirmed it's live code (newt_sendTask async WebSocket path), NOT dead code. No privacy decryption gap — PR #494
removes centralized decryption from both paths (operators decrypt locally now). - PR #489 CI fix: libs/regorus submodule pointed to unpushed commit c08b716. Pushed both missing commits (f6507c8 + c08b716) to regorus
remote main. CI re-triggered. - Privacy architecture analysis: User privacy data (KYC) flow is fully implemented with dual-signature linking. Policy client
confidential data (blacklists/allowlists) has domain constants defined but no upload/register/link/eval paths — the PrivacyRegistry is
built around a per-user linkage model that doesn't accommodate shared per-client datasets.
⏺ Excellent analysis. Here's the structured comparison:
User Privacy Data vs Policy Client Confidential Data
┌────────────────┬──────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────┐
│ Aspect │ User Privacy Data (KYC) │ Policy Client Confidential Data │
│ │ │ (Blacklist/Allowlist) │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Data owner │ Individual user (EOA) │ Compliance provider / protocol owner │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Data scope │ Per-user, per-domain │ Per-policy-client (shared across all users) │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Upload │ newt_uploadPrivacyData + user EIP-712 sig │ Not implemented │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ On-chain │ registerPrivacyData (gateway co-signs) │ Not implemented │
│ registration │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Linking │ 4 variants with dual-sig (linkPrivacyData*) │ N/A — data applies to all users, no per-user │
│ │ │ link needed │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Consent at │ Owner and/or user EIP-712 signatures │ Only client owner signature needed │
│ link time │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Consent at │ Implicit — intent signature proves user identity, operator looks │ N/A — no per-user consent needed │
│ eval time │ up policyClientLinks[client][intentSigner][domain] │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Evaluation │ policyClientLinks[client][intentSigner][domain] → owner → │ No path exists — lookup requires intentSigner │
│ lookup │ data_ref_id │ key, which doesn't apply │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Domain │ │ BLACKLIST/ALLOWLIST constants exist in │
│ dispatch │ Kyc variant in PrivacyDomain enum │ privacy_domains.rs but not connected to │
│ │ │ PrivacyDomain::from_bytes32 │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Revocation │ unlinkPrivacyDataAsSigner / unlinkPrivacyDataAsUser │ Not implemented │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Ephemeral │ _newton.privacy inline per-task │ Same, but impractical for large datasets │
│ fallback │ │ │
├────────────────┼──────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ Status │ Fully implemented │ Constants defined; all paths missing │
└────────────────┴──────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────┘
The Core Architectural Gap
★ Insight ─────────────────────────────────────
The PrivacyRegistry contract and fetch_privacy_data Rust function are built around a per-user linkage model:
policyClientLinks[policyClient][clientUser][privacyDomain]. This works perfectly for user privacy data where clientUser = the intent
signer.
For policy client confidential data (blacklists), there is no individual clientUser — the data applies to ALL users of that policy
client. The current lookup would return Address::ZERO and error because no per-user link exists.
Additionally, PrivacyDomain::from_bytes32() in chainio/src/privacy_data.rs:85-91 only recognizes keccak256(b"kyc"). The
BLACKLIST/ALLOWLIST constants in privacy_domains.rs use newton.privacy.blacklist naming — these are not connected to the dispatch enum.
─────────────────────────────────────────────────
What's Correctly Supported (User Privacy Flow)
The dual-signature linking is well-designed with 4 variants:
┌────────────────────────────────┬──────────────────────┬──────────────────────────┐
│ Variant │ Caller │ Sigs Required │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsSignerAndUser │ Same address is both │ None (msg.sender) │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsSigner │ Privacy owner │ User's EIP-712 sig │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyDataAsUser │ User │ Owner's EIP-712 sig │
├────────────────────────────────┼──────────────────────┼──────────────────────────┤
│ linkPrivacyData │ Any 3rd party │ Both owner AND user sigs │
└────────────────────────────────┴──────────────────────┴──────────────────────────┘
Consent at evaluation time is implicit: the operator recovers the intent signer from the intent EIP-712 signature, then uses that address
as the clientUser key in the on-chain lookup. If the link exists, the data was previously consented. No per-task consent directive
exists.
What's Missing (Policy Client Confidential Flow)
Three things need to happen to support blacklists/allowlists:
- Contract: Add a policy-client-level data path — either a new policyClientData[policyClient][domain] mapping or reuse policyClientLinks
with a sentinel address like address(1). Only the policy client owner would need to sign. - Rust resolution: fetch_privacy_data needs a fallback — if per-user policyClientLinks returns zero, check the policy-client-level
mapping. - Domain dispatch: Connect BLACKLIST/ALLOWLIST constants to PrivacyDomain enum variants and add deserialize_privacy_data arms.
Want me to spec out the contract changes and Rust resolution path for this, or create Linear tickets for the work?
⏺ Ran 4 stop hooks (ctrl+o to expand)
⎿ Stop hook error: [AUTOPILOT - Phase: unknown] Autopilot not complete. Continue working. When all phases are complete, run
/oh-my-claudecode:cancel to cleanly exit and clean up this session's autopilot state files. If cancel fails, retry with
/oh-my-claudecode:cancel --force.
⏺ Skill(oh-my-claudecode:cancel)
⎿ Successfully loaded skill
⏺ plugin:oh-my-claudecode:t - state_list_active (MCP)
⏺ plugin:oh-my-claudecode:t - state_clear (MCP)(mode: "autopilot", session_id: "5ef86c15-9c62-40c4-94a2-68d2f20e9af2")
⏺ Autopilot state cleared. Session complete.
Summary of what we covered today:
- lessons.md optimization: 40,817 → 32,472 chars (-20%). Removed 11 architectural-advice entries, merged 1 duplicate, condensed 7
verbose entries. All sacred failure-mode entries preserved. - channel.rs analysis: Confirmed it's live code (newt_sendTask async WebSocket path), NOT dead code. No privacy decryption gap — PR #494
removes centralized decryption from both paths (operators decrypt locally now). - PR #489 CI fix: libs/regorus submodule pointed to unpushed commit c08b716. Pushed both missing commits (f6507c8 + c08b716) to regorus
remote main. CI re-triggered. - Privacy architecture analysis: User privacy data (KYC) flow is fully implemented with dual-signature linking. Policy client
confidential data (blacklists/allowlists) has domain constants defined but no upload/register/link/eval paths — the PrivacyRegistry is
built around a per-user linkage model that doesn't accommodate shared per-client datasets.
✻ Baked for 4m 7s
❯ Contract: Add a policy-client-level data path — either a new policyClientData[policyClient][domain] mapping. One question: what if we
want 2 different policy clients co-owning the same privacy data by co-managing the data? We need to spec out the entire feature for
Policy Client Confidential Data. Also, I think our renaming from identity to privacy for identity registry was a mistake - instead,
privacy should refer policy client level private data vs identity being user private data. Or if you have a better idea in terms of
naming, please suggest. In terms of rego namespace, let's also separate between the two: data.identity vs data.privacy /clarify-plan
⏺ Skill(/clarify-plan)
⎿ Successfully loaded skill
⏺ Good — let me clarify the requirements before we spec anything. Three questions to start:
- Naming: Two registries or one?
You're proposing splitting the concept back into two namespaces: data.identity (user-level, e.g., KYC) vs data.privacy
(policy-client-level, e.g., blacklists). Should these be two separate on-chain contracts (IdentityRegistry + PrivacyRegistry), or one
contract with two data paths (like the current PrivacyRegistry with both user-linked and client-linked mappings)? Two contracts gives
cleaner separation but doubles deployment/upgrade surface. One contract with two mapping families keeps it consolidated but the naming
gets awkward.
- Co-ownership model for confidential data:
When two policy clients "co-manage" a blacklist, what does that mean operationally?
- (a) One owner uploads, both clients have read access (shared reference, single writer)
- (b) Both clients can update the same dataset independently (multi-writer, conflict resolution needed)
- (c) A third-party compliance provider owns it and grants access to both clients (provider-managed, clients are consumers)
This determines whether co-ownership is just shared read access to a data_ref_id, or if we need multi-sig writes and versioning.
- Update lifecycle for confidential data:
Blacklists change frequently (OFAC updates, new flagged addresses). How should updates work?
- (a) Immutable refs — new upload creates new data_ref_id, owner must re-link (current pattern for user data)
- (b) Mutable refs — same data_ref_id slot, owner can overwrite the encrypted blob in the DB without on-chain tx
- (c) Versioned — on-chain pointer to "latest" version, off-chain storage has version history
This affects gas costs, auditability, and how operators know they have the freshest data at eval time.
❯ 1. which one do you suggest? two separate? or single? 2. A third-party compliance provider owns it and grants access to both clients
(provider-managed, clients are consumers) 3. (c) Versioned — on-chain pointer to "latest" version, off-chain storage has version history
⏺ Good answers. Before I suggest on Q1, let me ask the next batch to solidify the model:
- Provider identity and trust:
For the provider-managed model (c), who is the "provider" on-chain? Is it:
- (a) Any EOA/multisig that registers as a data provider (new role, needs its own registry or just an address)
- (b) The policy client owner themselves acting as provider for their own client + granting to others
- (c) A whitelisted set of compliance providers (e.g., Chainalysis, Elliptic) managed by Newton governance
This determines whether we need a ProviderRegistry or if the provider is just an address with no special on-chain role beyond ownership
of the data.
- Access revocation granularity:
When a provider grants a blacklist to two policy clients and later wants to revoke one:
- (a) Provider can revoke per-client (unlink from Client B, keep Client A)
- (b) Provider can only revoke globally (delete the data, both lose access)
- (c) Both — provider can revoke per-client OR globally
And when a new version is published, do existing client links automatically point to the latest, or do clients need to explicitly
"accept" new versions?
- Back to Q1 — naming and contract structure:
Given your answers (provider-managed, versioned, two Rego namespaces), I'm leaning toward two separate contracts with clear naming:
┌──────────────────────────┬─────────────────┬───────────────────────────────────────────────────────┬───────────────────────────────┐
│ Contract │ Rego Namespace │ Data Model │ Example │
├──────────────────────────┼─────────────────┼───────────────────────────────────────────────────────┼───────────────────────────────┤
│ IdentityRegistry │ data.identity.* │ Per-user, user-linked, dual-sig consent │ KYC, credit score, │
│ │ │ │ accreditation │
├──────────────────────────┼─────────────────┼───────────────────────────────────────────────────────┼───────────────────────────────┤
│ ConfidentialDataRegistry │ data.privacy.* │ Per-provider, provider-managed, versioned, │ Blacklists, allowlists, │
│ │ │ multi-client grants │ sanctions │
└──────────────────────────┴─────────────────┴───────────────────────────────────────────────────────┴───────────────────────────────┘
The word "privacy" maps better to "confidential data that a provider manages and selectively shares" rather than "user's personal
identity data." And IdentityRegistry is what we had before — the rename to PrivacyRegistry conflated two distinct concepts.
Alternative naming: ConfidentialRegistry / ComplianceRegistry / DataVaultRegistry instead of ConfidentialDataRegistry. Any preference, or
does ConfidentialDataRegistry feel right?
❯ 4. (a) Any EOA/multisig that registers as a data provider (new role, needs its own registry or just an address) - think of it as
PrivatePolicyData (the data owner explicitly defines which policy client can use) vs policy data: any policy client can use 5. C 6.
ConfidentialDataRegistry sounds good. two separate
⏺ Clear model. The distinction is sharpening: IdentityRegistry (user owns, user consents) vs ConfidentialDataRegistry (provider owns,
provider grants). Last batch to nail down the spec:
- Provider registration — lightweight or formal?
For providers in the ConfidentialDataRegistry:
- (a) No registration needed — any address can upload and become a "provider" implicitly (like how anyone can deploy an ERC-20)
- (b) Lightweight self-registration — provider calls registerProvider() once to declare themselves, no approval needed, but creates an
on-chain identity for revocation/enumeration - (c) Approved by Newton governance — addProvider(address) callable only by contract owner/multisig
Given your "any EOA/multisig" answer, I'm leaning toward (b) — self-registration gives us an on-chain anchor for the provider without
gatekeeping, and lets us enumerate providers and their datasets.
- Version pointer behavior on grant:
You said versioned with on-chain pointer to "latest." When a provider publishes v2 of a blacklist:
- (a) All granted policy clients automatically see v2 at next evaluation (pointer is shared, "latest" is global)
- (b) Each client has its own version pointer — provider must explicitly push v2 to each client (more control, more gas)
- (c) Auto-update by default, but clients can "pin" to a specific version if they want stability
Option (a) is simplest and maps to the compliance use case well — when OFAC updates the sanctions list, everyone should see it
immediately. But (c) gives flexibility for clients who need deterministic evaluation across a window.
- Size and inline vs ref model:
Blacklists can be large (thousands of addresses). The current encrypted_data_refs table stores encrypted blobs off-chain with a
content-hash data_ref_id on-chain. For versioned confidential data:
- (a) Same pattern — each version is a new data_ref_id (content-hash), on-chain latestVersion pointer updates. Simple, immutable
versions, operators fetch by ref. - (b) Mutable off-chain slot keyed by (provider, domain) — no new data_ref_id per version, just overwrite. Less on-chain gas but no
version history. - (c) Off-chain versioned store with (provider, domain, version_number) key — on-chain stores latestVersion: uint64, operators fetch
version_number from chain then resolve off-chain.
I'd suggest (a) for simplicity — reuses the existing encrypted_data_refs infrastructure, each version is immutable and auditable, and the
on-chain pointer swap is a single SSTORE.
❯ 7. B 8. a 9. a
⏺ All clear. I have everything needed to spec this out. Here's the consolidated requirements before I write the plan:
Confirmed Architecture
┌───────────────────┬──────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────┐
│ Aspect │ IdentityRegistry │ ConfidentialDataRegistry │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Rego namespace │ data.identity.* │ data.privacy.* │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Data owner │ Individual user (EOA) │ Provider (any self-registered EOA/multisig) │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Data scope │ Per-user, per-domain │ Per-provider, per-domain, versioned │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Consent model │ Dual-sig linking (user + owner) │ Provider grants to policy clients │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Co-ownership │ N/A │ Provider grants same dataset to multiple clients │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Update model │ Immutable refs, re-link on update │ Versioned — new data_ref_id per version, on-chain latestVersion │
│ │ │ pointer, auto-visible to all granted clients │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Revocation │ Per-user unlink │ Per-client revoke OR global revoke │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Provider │ N/A │ Self-registration (registerProvider()) │
│ registration │ │ │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Off-chain storage │ Reuses encrypted_data_refs │ Same — each version is a new data_ref_id │
├───────────────────┼──────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
│ Contract │ Existing (revert rename back from │ New contract │
│ │ PrivacyRegistry) │ │
└───────────────────┴──────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────┘
Implications for Current PR Stack
- PR #489 (refactor: rename IdentityRegistry to PrivacyRegistry) — this rename should be reverted. The contract stays as IdentityRegistry
for user identity data. - PR #494 (feat: KMS to HPKE) — the functional HPKE migration is correct and stays. References to "PrivacyRegistry" in docs/code would
need updating once #489 is reverted. - New work — ConfidentialDataRegistry contract, Rust resolution path, provider self-registration, versioned pointer, grant/revoke
per-client, data.privacy.* Rego injection.
Want me to write up the full implementation plan for the ConfidentialDataRegistry feature? I can spec the contract interface, Rust
changes, Rego integration, and E2E test plan — then we can create Linear tickets from it.
⏺ Now let me look at deserialize_identity_data and the evaluate() identity dispatch to understand where the rigidity is.
Read 2 files (ctrl+o to expand)
⏺ Now I see the full picture. Here's the breakdown:
★ Insight ─────────────────────────────────────
The evaluate() function (line 173) already has a generic fallback for unknown identity domains — register_generic_identity_extensions
gives newton.identity.get("field"). The problem is upstream in deserialize_identity_data() (line 208) which errors out on unknown domains
before evaluate() ever sees the data.
So evaluate() is flexible, but deserialize_identity_data() is the bottleneck.
─────────────────────────────────────────────────
The fix: in resolve_identity_data (operator core.rs), if deserialize_identity_data fails on an unknown domain, fall back to a
GenericIdentityData wrapper — same pattern as GenericConfidentialData. The JSON fields become accessible via
newton.identity.get("field_name") even without domain-specific builtins.
But there's an important detail — IdentityDomainData requires reference_date() for time-based comparisons, while ConfidentialDomainData
doesn't. A generic wrapper would need to provide a timestamp even for non-time-based domains.
Let me make this change. It's a small refactor in resolve_identity_data:
Read 1 file (ctrl+o to expand)
⏺ Now I see it. Line 702 calls deserialize_identity_data which fails on unknown domains. The fix: catch the error and fall back to a
generic wrapper.
Read 1 file (ctrl+o to expand)
⏺ Good — IdentityDomainData is re-exported. Now let me create a GenericIdentityData wrapper and add the fallback.
⏺ Update(crates/operator/src/core.rs)
⎿ Added 53 lines, removed 2 lines
699 .format("%Y-%m-%d")
700 .to_string();
701
702 - let rego_data = deserialize_identity_data(&domain, &decrypted_json, date)
703 - .map_err(|e| OperatorError::IdentityFetchError(e.to_string()))?;
702 + let rego_data = match deserialize_identity_data(&domain, &decrypted_json, date.clone()) {
703 + Ok(data) => data,
704 + Err(e) => {
705 + // Unknown domain: fall back to generic wrapper so newton.identity.get("field")
706 + // works for arbitrary domain data without needing domain-specific builtins.
707 + let domain_name = newton_prover_chainio::identity_data::IdentityDomain::from_bytes32(&domain)
708 + .map(|d| d.name().to_string())
709 + .unwrap_or_else(|| format!("0x{}", hex::encode(&domain[..8])));
710 + warn!(
711 + domain = %domain_name,
712 + error = %e,
713 + "unknown identity domain, using generic accessor fallback"
714 + );
715 + let value: serde_json::Value = serde_json::from_str(&decrypted_json)
716 + .map_err(|e| OperatorError::IdentityFetchError(format!("identity data not valid JSON: {e}")))?;
717 + Box::new(GenericIdentityData {
718 + domain: domain_name,
719 + reference_date: date,
720 + fields: value,
721 + })
722 + }
723 + };
724
725 Ok(Some(rego_data))
726 }
727
728 +/// Generic identity data wrapper for unknown/custom domains.
729 +///
730 +/// Enables the newton.identity.get(field_name) Rego accessor for arbitrary identity
731 +/// domain data without requiring domain-specific builtins like newton.identity.kyc.age_gte().
732 +#[derive(Debug)]
733 +struct GenericIdentityData {
734 + domain: String,
735 + reference_date: String,
736 + fields: serde_json::Value,
737 +}
738 +
739 +impl IdentityDomainData for GenericIdentityData {
740 + fn domain_name(&self) -> &str {
741 + &self.domain
742 + }
743 +
744 + fn reference_date(&self) -> &str {
745 + &self.reference_date
746 + }
747 +
748 + fn to_field_map(&self) -> std::collections::BTreeMap<String, newton_prover_core::rego::Value> {
749 + let mut map = std::collections::BTreeMap::new();
750 + if let Some(obj) = self.fields.as_object() {
751 + for (k, v) in obj {
752 + map.insert(k.clone(), json_to_regorus_value(v));
753 + }
754 + }
755 + map
756 + }
757 +}
758 +
759 /// Generic confidential data wrapper for unknown/custom domains.
760 ///
761 /// Used when the domain name does not match a known built-in (e.g., "blacklist", "allowlist").
⏺ Bash(cargo check -p newton-prover-operator 2>&1 | grep "error[" | head -5)
⎿ (No output)
⎿ (timeout 2m)
⏺ Clean build. Let me commit and push.
⏺ Bash(git add crates/operator/src/core.rs && git commit -m "$(cat <<'EOF'
feat: add generic identity data fallback for unknown domains…)
⎿ [feat/kms-to-hpke-migration 76d69c00] feat: add generic identity data fallback for unknown domains
1 file changed, 53 insertions(+), 2 deletions(-)
remote:
… +6 lines (ctrl+o to expand)
⏺ Done. Now both identity and confidential data have the same architecture:
┌──────────────┬───────────────────────────────────────────────────┬───────────────────────────────────────────────┐
│ │ Known Domain │ Unknown Domain │
├──────────────┼───────────────────────────────────────────────────┼───────────────────────────────────────────────┤
│ Identity │ newton.identity.kyc.age_gte(21) etc. (8 builtins) │ newton.identity.get("field") generic accessor │
├──────────────┼───────────────────────────────────────────────────┼───────────────────────────────────────────────┤
│ Confidential │ newton.privacy.blacklist.contains(addr) etc. │ newton.privacy.get("field") generic accessor │
└──────────────┴───────────────────────────────────────────────────┴───────────────────────────────────────────────┘
Policy authors can use new domains immediately via the generic get() accessor — no Rust code changes needed. Domain-specific builtins get
added later when the domain's computation needs justify them.
✻ Sautéed for 1m 46s
❯ But there's an important detail — IdentityDomainData requires reference_date() for time-based comparisons, while ConfidentialDomainData
doesn't. A generic wrapper would need to provide a timestamp even for non-time-based domains. --> confidential data could also have time
based comparisons. how can we provide generic support for it in our rego support?
⏺ You're right — a compliance dataset could have last_aml_check: "2026-03-15" that needs "is this within 90 days?". The current split —
IdentityDomainData has reference_date(), ConfidentialDomainData doesn't — is arbitrary.
The real question is: should reference_date be a trait requirement or a data field?
★ Insight ─────────────────────────────────────
The reference_date exists because Rego has no date parsing. The operator computes reference_date from task.initializationTimestamp
(on-chain) and passes it alongside the data. KYC builtins like age_gte(21) use it internally: now - birthdate >= 21 years.
For confidential data to support time-based comparisons, we have two options:
- Add reference_date to ConfidentialDomainData trait — same pattern as identity. Every confidential domain gets a timestamp, even if
unused. - Create a shared Rego builtin newton.time.date_diff_days(date_a, date_b) that any policy can use, regardless of domain. Then date
arithmetic isn't coupled to the data trait at all — it's a general-purpose utility.
Option 2 is better architecture — date comparison is a computation capability, not a property of the data. A policy author should be able
to write:
allow {
days_since := newton.time.days_since(data.privacy.sanctions.last_check, input.reference_date)
days_since <= 90
}
─────────────────────────────────────────────────
Option 2 decouples date arithmetic from the data domain entirely. Let me implement it as a new set of time utility builtins in regorus,
available to all policies regardless of whether they use identity, confidential, or neither.
⏺ oh-my-claudecode:executor(Add newton.time Rego builtins) Sonnet 4.6
⎿ Done (44 tool uses · 131.1k tokens · 5m 2s)
(ctrl+o to expand)
⏺ Time builtins created — 55 tests passing. Let me verify the full workspace and commit everything.
⏺ Bash(cargo check --workspace 2>&1 | grep "error[" | head -5)
⎿ (No output)
⎿ (timeout 5m)
⏺ Clean build. Let me commit the regorus changes first, then the monorepo.
⏺ Bash(cd /Users/dennis.won/projects/newton-prover-avs/libs/regorus && git add src/extensions/time.rs src/extensions/mod.rs src/lib.rs
src/engine.rs Cargo.toml && git…)
⎿ [main 596c11e] feat: add newton-time extension with general-purpose date arithmetic builtins
5 files changed, 315 insertions(+), 1 deletion(-)
create mode 100644 src/extensions/time.rs
… +10 lines (ctrl+o to expand)
❯ why do we need separate identity data vs confidential data? /// * data - The data to evaluate the policy against
/// * input - The input to evaluate the policy against
/// * identity_data - Optional domain-flexible identity data for identity-enabled policies
/// * confidential_data - Optional domain-flexible confidential data for privacy-enabled policies
/// * rule - The rule to evaluate
/// * additional_data - Optional JSON object merged into the Rego data namespace
/// # Returns
data: &str,
input: &str,
identity_data: Option<Box>,
confidential_data: Option<Box>, make sure throughout codebase, we don't unnecessarily introduce
duplicatively, consolidate-able codes between identity data handling vs confidential data handling
⏺ Now commit the monorepo changes and update the submodule pointer.
⏺ Bash(cd /Users/dennis.won/projects/newton-prover-avs && git add libs/regorus crates/core/Cargo.toml crates/core/src/rego/mod.rs && git
commit -m "feat: add newton.ti…)
⎿ [feat/kms-to-hpke-migration e31d0955] feat: add newton.time general-purpose date builtins and update regorus submodule
3 files changed, 29 insertions(+), 20 deletions(-)
remote:
… +6 lines (ctrl+o to expand)
⏺ Now to address your question — you're making a critical architectural point. Let me analyze the duplication.
★ Insight ─────────────────────────────────────
You're right. IdentityDomainData and ConfidentialDomainData are nearly identical traits:
// Identity
trait IdentityDomainData: Send + Sync + Debug {
fn domain_name(&self) -> &str;
fn reference_date(&self) -> &str; // only difference
fn to_field_map(&self) -> BTreeMap<String, Value>;
}
// Confidential
trait ConfidentialDomainData: Send + Sync + Debug {
fn domain_name(&self) -> &str;
fn to_field_map(&self) -> BTreeMap<String, Value>;
}
The only difference is reference_date() — and with the new newton.time.* builtins, even that's unnecessary. Policies can pass dates
explicitly: newton.time.age_years(data.identity.kyc.birthdate, input.reference_date).
─────────────────────────────────────────────────
The duplication runs deep across:
- Traits: IdentityDomainData vs ConfidentialDomainData — nearly identical
- Generic wrappers: GenericIdentityData vs GenericConfidentialData in core.rs — same struct, same logic
- evaluate() parameters: two separate Option<Box<dyn ...>> params
- Extension registration: two parallel blocks in evaluate() doing the same dispatch pattern
- Operator resolution: resolve_identity_data and resolve_confidential_data — same fetch→decrypt→deserialize flow
- DB methods: insert_identity / insert_confidential — same SQL pattern with data_type discriminator
- Gateway handlers: upload_identity_encrypted / upload_confidential_data — same store+return flow
These should be unified into a single PrivacyDomainData trait (or PolicyData trait) with a data_type discriminator. The evaluate()
function takes one Vec<Box> instead of two separate optional params.
This is a significant refactor though. Want me to tackle it now, or save it for a dedicated refactor PR? It would touch the regorus
extension traits, operator core, gateway handlers, and the evaluate pipeline.
✻ Churned for 8m 8s
❯ tackle now
⏺ This is a deep refactor. Let me plan the unified design, then execute.
The key insight: the only real difference between identity and confidential data at the trait level is the Rego namespace prefix
(identity vs privacy). Everything else — domain name, field map, generic accessor, dispatch pattern — is identical.
Unified design:
// One trait to rule them all
pub trait PolicyDomainData: Send + Sync + Debug {
fn domain_name(&self) -> &str; // "kyc", "blacklist", "allowlist"
fn rego_prefix(&self) -> &str; // "identity" or "privacy"
fn to_field_map(&self) -> BTreeMap<String, Value>;
}
This collapses:
- 2 traits → 1
- 2 evaluate params → 1 Vec
- 2 generic wrappers → 1
- 2 dispatch blocks in evaluate → 1 loop
- 2 resolve functions → shared logic
⏺ oh-my-claudecode:executor(Unify identity + confidential traits) Opus 4.6
⎿ Read(crates/core/src/common/task.rs)
Read(crates/operator/src/core.rs)
Read(crates/operator/src/core.rs)
+18 more tool uses (ctrl+o to expand)
newt_getPrivacyPublicKey serves BOTH Phase 1 and Phase 2
Looking at handler/mod.rs:1375-1389, the function already handles both modes:
fn get_privacy_public_key(&self) -> Result<serde_json::Value> {
// When threshold decryption is configured, return the threshold master
// public key (MPK) so clients encrypt to the threshold group key.
#[cfg(feature = "threshold")]
{
let tctx_guard = self.threshold_ctx.load();
if let Some(tctx) = tctx_guard.as_deref() {
let pk_hex = hex::encode(tctx.public_key.hpke_public_key);
return Ok(json!({ "public_key": pk_hex, ... }));
}
}
// Fallback: return the gateway's standalone HPKE key (Phase 1)
...
}
Phase 1: Returns the gateway's standalone HPKE public key (derived from ENCRYPTION_PRIVATE_KEY). All operators share this key and decrypt
locally.
Phase 2 (threshold): Returns the threshold master public key (MPK) — the combined public key from the DKG ceremony. No single operator
has the corresponding private key.
The Phase 2 threshold encryption/upload flow
From the client/provider perspective, the flow is identical to Phase 1 — they don't know or care about threshold vs centralized. They
call newt_getPrivacyPublicKey, get a public key, and encrypt to it:
Client/Provider Gateway Operators
| | |
|--- newt_getPrivacyPublicKey -------->| |
|<-- MPK (threshold master pubkey) ----| |
| | |
|--- HPKE encrypt to MPK ------------>| |
|--- newt_uploadConfidentialData ----->| |
|<-- data_ref_id ---------------------| |
| | |
|--- publishData(domain, ref) ------->| (on-chain) |
| | |
|--- newt_createTask ---------------->| |
| |--- Prepare phase ----------->|
| |<-- partial DH outputs -------|
| | |
| |--- Lagrange combine -------->|
| | (gateway has plaintext) |
| | |
| |--- Commit phase ------------>|
| | (threshold_privacy: data) |
| |<-- BLS signatures ----------|
The key difference is who decrypts:
- Phase 1: Each operator decrypts locally (they all share the same private key)
- Phase 2: No single entity has the private key. During Prepare phase, each operator computes a partial DH output using their key share.
The gateway combines these via Lagrange interpolation to recover the shared secret, then completes the HPKE decryption. The plaintext is
passed via threshold_privacy in the Commit request.
★ Insight ─────────────────────────────────────
The beauty of this design: clients are completely unaware of which decryption mode is active. They encrypt to whatever
newt_getPrivacyPublicKey returns. The gateway transparently switches between centralized (Phase 1) and threshold (Phase 2) decryption
based on whether threshold_ctx is populated from a completed DKG ceremony.
The comment on newt_getPrivacyPublicKey should be updated to reflect this dual-mode behavior — currently it only says "shared X25519 HPKE
public key" without mentioning the threshold MPK path.
─────────────────────────────────────────────────
Cross-Repo PR Review
Deploy PR #188 — Needs minor update
The deploy PR correctly:
- Removes KMS IAM policy
- Adds GATEWAY__ENCRYPTION__PRIVATE_KEY and OPERATOR__ENCRYPTION__PRIVATE_KEY
- References AVS PR #489
Gap: The deploy comment says "Shared HPKE encryption key for privacy, identity, and secrets data" — should also say "and confidential
data" (4 types now). Minor comment fix. The CDK reads deployment JSONs for contract addresses but doesn't need to explicitly know about
confidentialDataRegistry since contract addresses are consumed by the Rust binary (which reads them from the deployment JSON at startup),
not by the CDK.
Verdict: In sync. Optional comment update.
SDK PR #146 — Needs uploadConfidentialData support
The SDK PR correctly migrates storeEncryptedSecrets from KMS to HPKE. But it doesn't yet support:
- newt_uploadConfidentialData — providers need an SDK function to upload encrypted confidential data
- newt_getConfidentialData — retrieval function
The SDK already has the HPKE primitives (createSecureEnvelope, getPrivacyPublicKey). Adding these two functions is straightforward.
Verdict: Functional for secrets migration. Needs follow-up for confidential data support.
Identity PR #19 — In sync
The identity PR correctly:
- Replaces RSA-OAEP with HPKE via SDK's createSecureEnvelope
- Fetches gateway HPKE public key via getPrivacyPublicKey
- Removes NEXT_PUBLIC_KMS_PUBLIC_KEY env var
- Bumps SDK to ^0.7.1
- Wire format matches our UploadIdentityEncryptedRequest
Verdict: Fully in sync. No changes needed.
Remaining Items to Complete the Migration
┌─────────────────────────────────────────┬──────────────────────────────┬──────────────────┐
│ Item │ Status │ Blocker? │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ AVS PR #489 (this PR) │ Ready for review │ No │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ Deploy PR #188 │ Ready (minor comment update) │ No │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ SDK PR #146 (secrets HPKE) │ Ready │ No │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ Identity PR #19 (client HPKE) │ Ready │ No │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ SDK: uploadConfidentialData function │ Not started │ No — follow-up │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ Store encryption key in Secrets Manager │ Manual step before deploy │ Yes — pre-deploy │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ NEWT-878 (Cross-repo E2E) │ Blocked on merge + deploy │ Post-merge │
├─────────────────────────────────────────┼──────────────────────────────┼──────────────────┤
│ Squash/clean commit history │ Optional │ No │
└─────────────────────────────────────────┴──────────────────────────────┴──────────────────┘
Deploy Order
- cast wallet new → store hex in Secrets Manager as GATEWAY__ENCRYPTION__PRIVATE_KEY and OPERATOR__ENCRYPTION__PRIVATE_KEY
- Merge deploy PR #188
- Merge AVS PR #489 (triggers stagef deploy)
- Publish SDK (merge PR #146) — version 0.7.2
- Merge identity PR #19
- Cross-repo E2E validation (NEWT-878)
The SDK uploadConfidentialData function can ship as a separate SDK PR after the core migration lands — providers can call the RPC
directly via curl or the SDK's generic callRpc in the meantime.
Want me to add comments to the cross-repo PRs noting the ConfidentialDataRegistry addition, or save the progress to memory first?
Created NEWT-898 in the Newton Privacy Layer project — backlog priority, dual-encryption approach for client-side secret retrieval.
Here's where everything stands:
KMS-to-HPKE migration PRs (all 4 repos):
┌──────────────────────────┬──────┬──────────────────┐
│ Repo │ PR │ Status │
├──────────────────────────┼──────┼──────────────────┤
│ newton-prover-avs │ #489 │ Under review │
├──────────────────────────┼──────┼──────────────────┤
│ newton-prover-avs-deploy │ #188 │ Under review │
├──────────────────────────┼──────┼──────────────────┤
│ newton-sdk │ #146 │ Ready for review │
├──────────────────────────┼──────┼──────────────────┤
│ newton-identity │ #19 │ Ready for review │
└──────────────────────────┴──────┴──────────────────┘
Follow-ups: