-
-
Save denniswon/4585e61569dfc6b23174682897656cdf to your computer and use it in GitHub Desktop.
| --- | |
| Good context gathered. I can see the current flow is: | |
| 1. newton-identity encrypts with RSA-OAEP (AWS KMS public key) → hex ciphertext | |
| 2. Signs the encrypted string with EIP-712 (EncryptedIdentityData) | |
| 3. Gateway verifies EIP-712 signature, then stores the encrypted data on-chain via submitIdentity | |
| 4. At evaluation time, operators decrypt via AWS KMS | |
| The Newton SDK already has a full HPKE privacy module (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) with AAD binding, Ed25519 signing, and | |
| uploadEncryptedData RPC. | |
| Here are my first batch of questions: | |
| 1. Gateway RPC method change — Currently newton-identity calls newt_sendIdentityEncrypted which stores the RSA-OAEP ciphertext directly on-chain. | |
| After migration, should identity data go through the same newt_uploadEncryptedData RPC that the SDK's privacy module uses (storing via data_ref_id | |
| pattern), or do we keep newt_sendIdentityEncrypted but change it to accept HPKE-encrypted envelopes instead of RSA ciphertext? The former unifies | |
| the code paths; the latter keeps identity data storage separate from general privacy data. | |
| 2. EIP-712 signing vs Ed25519 signing — The current flow EIP-712-signs the encrypted data (EncryptedIdentityData(string data)) using the Identity | |
| Owner EOA via Turnkey. The SDK privacy module uses Ed25519 signing instead (separate key pair from the EVM wallet). For the migration, should we | |
| keep EIP-712 signing with the Turnkey wallet (simpler UX — no second key pair), switch to Ed25519 like the SDK privacy module (consistent with the | |
| privacy layer), or use both (HPKE encrypt + Ed25519 sign the envelope, but also EIP-712 sign for on-chain identity binding)? | |
| 3. On-chain storage model — Currently the IdentityRegistry.submitIdentity(owner, domain, data) stores the encrypted string directly in contract | |
| storage. The Privacy Layer's uploadEncryptedData stores data off-chain in the gateway and returns a data_ref_id. For identity data, do we keep | |
| on-chain storage (contract state) with just the encryption scheme changing, or move to the off-chain data_ref_id pattern like the rest of the | |
| privacy layer? On-chain gives permanence/auditability; off-chain gives flexibility and lower gas costs. | |
| --- | |
| Good context gathered. I can see the current flow is: | |
| 1. newton-identity encrypts with RSA-OAEP (AWS KMS public key) → hex ciphertext | |
| 2. Signs the encrypted string with EIP-712 (EncryptedIdentityData) | |
| 3. Gateway verifies EIP-712 signature, then stores the encrypted data on-chain via submitIdentity | |
| 4. At evaluation time, operators decrypt via AWS KMS | |
| The Newton SDK already has a full HPKE privacy module (X25519 + HKDF-SHA256 + ChaCha20-Poly1305) with AAD binding, Ed25519 signing, and | |
| uploadEncryptedData RPC. | |
| Here are my first batch of questions: | |
| 1. Gateway RPC method change — Currently newton-identity calls newt_sendIdentityEncrypted which stores the RSA-OAEP ciphertext directly on-chain. | |
| After migration, should identity data go through the same newt_uploadEncryptedData RPC that the SDK's privacy module uses (storing via data_ref_id | |
| pattern), or do we keep newt_sendIdentityEncrypted but change it to accept HPKE-encrypted envelopes instead of RSA ciphertext? The former unifies | |
| the code paths; the latter keeps identity data storage separate from general privacy data. | |
| 2. EIP-712 signing vs Ed25519 signing — The current flow EIP-712-signs the encrypted data (EncryptedIdentityData(string data)) using the Identity | |
| Owner EOA via Turnkey. The SDK privacy module uses Ed25519 signing instead (separate key pair from the EVM wallet). For the migration, should we | |
| keep EIP-712 signing with the Turnkey wallet (simpler UX — no second key pair), switch to Ed25519 like the SDK privacy module (consistent with the | |
| privacy layer), or use both (HPKE encrypt + Ed25519 sign the envelope, but also EIP-712 sign for on-chain identity binding)? | |
| 3. On-chain storage model — Currently the IdentityRegistry.submitIdentity(owner, domain, data) stores the encrypted string directly in contract | |
| storage. The Privacy Layer's uploadEncryptedData stores data off-chain in the gateway and returns a data_ref_id. For identity data, do we keep | |
| on-chain storage (contract state) with just the encryption scheme changing, or move to the off-chain data_ref_id pattern like the rest of the | |
| privacy layer? On-chain gives permanence/auditability; off-chain gives flexibility and lower gas costs. | |
| ✻ Cogitated for 1m 9s | |
| ❯ 1.What are pros vs cons of storing data onchain on identity registry contract vs. storing in our database. Help me brainstorm whether we should | |
| merge identity storage separate from general privacy data. Would it be viable to store identity data through the same path as other general privacy | |
| data but still use Identity Registry for just link data between users and policy clients? Would that have advantage of gas saving and potentially | |
| also cross-chain interoperability in the future? 2. most ideal if we can just use Ed25519 because we are for sure migrate from AWS KMS to Newton | |
| privacy layer? is that possible to use Turnkey still but also comply with privacy module? If not, HPKE encrypt + Ed25519 sign the envelope, but | |
| also EIP-712 sign for on-chain identity binding should also be okay, but UX might not be ideal. One question is what happens to the encrypted HPKE | |
| data in case the identity owner who encrypted the data "loses" their ed25519 key? Or am i missing something here? 3. On-chain gives | |
| permanence/auditability; off-chain gives flexibility and lower gas costs. --> let's dive and brainstorm more. Definitely flexibility and lower gas | |
| costs highly preferred, especailly in Multichain setting, UX is one of the most important aspect. My thinking is that if we move to off chain | |
| data_ref_id and just store the link and the data_ref_id instead of the entire encrypted data, it will be ultimately long term better solution but | |
| I need you to objectively challenge/review my ideas and thinking here | |
| ⏺ These are great questions with real architectural trade-offs. Let me work through each one systematically. | |
| --- | |
| 1. On-Chain Identity Registry vs. Off-Chain Privacy Data Store | |
| Current Architecture | |
| Identity Data: newton-identity → RSA-OAEP encrypt → EIP-712 sign → Gateway → submitIdentity() → on-chain storage | |
| Privacy Data: newton-sdk → HPKE encrypt → Ed25519 sign → Gateway → uploadEncryptedData → off-chain (DB) | |
| The Hybrid Model You're Proposing | |
| Keep IdentityRegistry for link data only (who is linked to whom), move encrypted identity data to the off-chain privacy data store (same path as | |
| general privacy data). | |
| Link data: IdentityRegistry.linkIdentity() → on-chain (policyClientLinks mapping) | |
| Identity data: uploadEncryptedData (HPKE envelope) → off-chain (data_ref_id in gateway DB) | |
| Reference: IdentityRegistry stores data_ref_id → on-chain pointer to off-chain data | |
| Pros of the Hybrid Model | |
| ┌─────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Benefit │ Detail │ | |
| ├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Gas savings │ Storing a 32-byte data_ref_id on-chain vs. ~512+ bytes of RSA ciphertext is 10-15x cheaper. On Mainnet at current │ | |
| │ │ gas prices, this matters significantly per identity registration. │ | |
| ├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Cross-chain │ A data_ref_id is chain-agnostic. The encrypted data lives in the gateway, and any chain's IdentityRegistry can │ | |
| │ interoperability │ reference it. No need to replicate encrypted blobs across chains. │ | |
| ├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Unified encryption │ One HPKE encrypt/decrypt path for both privacy data and identity data. One key rotation scheme, one audit │ | |
| │ infra │ surface, one set of operator decryption logic. │ | |
| ├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Data update flexibility │ Updating identity data (re-KYC, address change) doesn't require an on-chain tx — just upload a new envelope, get │ | |
| │ │ a new data_ref_id, and update the on-chain pointer. │ | |
| ├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ TTL/expiration │ Off-chain store can enforce TTLs on identity data (GDPR "right to be forgotten"), which is much harder with │ | |
| │ │ on-chain storage. │ | |
| └─────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| Cons / Risks to Challenge Your Thinking | |
| Risk: Gateway availability = data availability | |
| Detail: If the gateway is down, operators can't decrypt identity data for policy evaluation. On-chain storage is always available to any node. | |
| Mitigation: Gateway already must be available for task evaluation. Identity data access is already gated behind the gateway's decrypt path. This | |
| isn't a new dependency. | |
| ──────────────────────────────────────── | |
| Risk: Data permanence | |
| Detail: On-chain data survives even if the Newton gateway shuts down. Off-chain data depends on the gateway's DB durability. | |
| Mitigation: Use replicated storage (S3/IPFS backing), or store the HPKE envelope itself in a content-addressed store. The data_ref_id could be a | |
| content hash. | |
| ──────────────────────────────────────── | |
| Risk: Auditability | |
| Detail: On-chain storage provides a public, immutable record that identity data was submitted. Off-chain loses this. | |
| Mitigation: The IdentityBound event can still be emitted when a data_ref_id is registered on-chain. The event + on-chain ref provides the audit | |
| trail. The actual encrypted blob being off-chain doesn't weaken the audit — nobody could read the on-chain blob anyway. | |
| ──────────────────────────────────────── | |
| Risk: Contract migration | |
| Detail: IdentityRegistry.submitIdentity() currently stores the full encrypted string. Changing it to store a data_ref_id requires a contract | |
| upgrade or new function. | |
| Mitigation: Add a new function registerIdentityDataRef(owner, domain, dataRefId) alongside the existing one. Deprecate submitIdentity over time. | |
| ──────────────────────────────────────── | |
| Risk: Atomicity | |
| Detail: Currently, one tx stores the data. With the hybrid model, you upload first (off-chain), then register the ref (on-chain). If the second | |
| step fails, you have orphaned off-chain data. | |
| Mitigation: The orphaned data has a TTL and gets cleaned up. Or make the gateway handle both steps atomically behind a single RPC call. | |
| My Assessment | |
| Your instinct is right — the hybrid model is the better long-term architecture. The key insight is: nobody reads encrypted blobs from on-chain | |
| storage directly. Operators always go through the gateway to decrypt. So the on-chain encrypted data provides no additional availability over an | |
| off-chain store backed by the same gateway. | |
| The data_ref_id pattern is strictly better for: | |
| - Multi-chain (one data blob, many chain references) | |
| - Gas costs (32 bytes vs 500+ bytes) | |
| - Data updates (new envelope, update pointer) | |
| - Compliance (TTL, deletion) | |
| The one thing to nail down: should data_ref_id be a content hash of the envelope (content-addressed, deterministic) or a UUID (opaque, | |
| gateway-assigned)? Content hash gives you dedup and verifiability. UUID gives the gateway more control over storage lifecycle. | |
| --- | |
| 2. Ed25519 vs. EIP-712 Signing & Key Loss | |
| Can Turnkey Support Ed25519? | |
| Turnkey's signRawPayload signs arbitrary bytes with the wallet's key. But Turnkey wallets are secp256k1 (Ethereum) — not Ed25519. Turnkey doesn't | |
| offer Ed25519 key generation or signing. | |
| So the options are: | |
| Option A: Derive Ed25519 key from Turnkey wallet | |
| - Sign a deterministic message with the Turnkey secp256k1 key (e.g., "newton-privacy-key-derivation-v1") | |
| - Use the signature as entropy to derive an Ed25519 key pair | |
| - Pro: No second key for the user to manage. Deterministic — same wallet always produces the same Ed25519 key. | |
| - Con: Non-standard. The Ed25519 key is bound to the Turnkey wallet — if user migrates wallets, they need to re-derive. | |
| Option B: Generate standalone Ed25519 key pair | |
| - Use generateSigningKeyPair() from the SDK privacy module | |
| - Store the private key... where? The user is in a popup — no persistent storage. | |
| - This doesn't work for the popup model unless the integrating app manages the key. | |
| Option C: HPKE encrypt + Ed25519 sign + EIP-712 sign (belt and suspenders) | |
| - HPKE encrypt the data (privacy layer encryption) | |
| - Ed25519 sign the envelope (privacy layer auth — app/SDK provides this) | |
| - EIP-712 sign the data_ref_id or envelope hash (on-chain identity binding via Turnkey) | |
| - Pro: Identity binding remains on-chain verifiable via EIP-712, encryption is privacy-layer standard | |
| - Con: Two signatures per operation | |
| My Recommendation | |
| Option A (derived Ed25519) is the cleanest UX if viable. The derivation scheme would be: | |
| // In the popup, after Turnkey auth: | |
| const derivationMessage = "newton:ed25519:identity-privacy:v1"; | |
| const secp256k1Sig = await turnkeyProvider.request({ | |
| method: 'personal_sign', | |
| params: [derivationMessage, ownerAddress] | |
| }); | |
| // Use first 32 bytes of signature hash as Ed25519 seed | |
| const ed25519Seed = keccak256(secp256k1Sig).slice(0, 32); | |
| This is deterministic (same owner always gets the same Ed25519 key) and requires no key storage. | |
| What Happens If the Ed25519 Key Is "Lost"? | |
| With Option A, the Ed25519 key is derived from the Turnkey wallet — it can't be lost independently. If the user loses their Turnkey wallet, | |
| they've lost their identity authority entirely (the Identity Owner EOA is gone). | |
| With standalone Ed25519 keys: the encrypted data becomes unrecoverable for re-signing purposes, BUT the gateway can still decrypt (it has the HPKE | |
| private key). The Ed25519 key is for sender authentication, not decryption. So: | |
| - Data decryption: Not affected by Ed25519 key loss (HPKE recipient key is the gateway's) | |
| - Re-uploading/re-signing: Would require a new Ed25519 key pair and re-encryption | |
| - Existing data_ref_id references: Still valid, still decryptable | |
| The Ed25519 key is not a decryption key — it's a signing key proving who encrypted the data. Losing it means you can't prove authorship of future | |
| uploads, but existing data remains accessible. | |
| --- | |
| 3. On-Chain vs. Off-Chain Deep Dive | |
| Scenario Comparison | |
| Let me walk through concrete scenarios to stress-test the off-chain data_ref_id model: | |
| Scenario: User registers identity data on Sepolia, then an app on Base wants to verify | |
| ┌──────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Model │ What Happens │ | |
| ├──────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Current │ Data is in Sepolia's IdentityRegistry. Base operators must cross-chain read from Sepolia, or data must be replicated to │ | |
| │ (on-chain) │ Base's registry (another on-chain tx + gas). │ | |
| ├──────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Off-chain │ Data is in the gateway DB. Sepolia's registry has data_ref_id = 0xabc.... Base's registry can store the same │ | |
| │ data_ref_id │ data_ref_id. Operators on any chain query the gateway with the ref. One upload, N chain references. │ | |
| └──────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| Scenario: User needs to update KYC data (annual re-verification) | |
| ┌───────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Model │ What Happens │ | |
| ├───────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Current │ New on-chain tx to overwrite identityData[owner][domain]. Gas cost on every chain where data is stored. │ | |
| ├───────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Off-chain │ Upload new envelope → new data_ref_id. One on-chain tx to update the pointer. Old data can be TTL-expired. │ | |
| └───────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| Scenario: Regulatory deletion request (GDPR) | |
| ┌───────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Model │ What Happens │ | |
| ├───────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Current │ On-chain data is immutable. Even if you overwrite with empty bytes, the old data is in historical state. Technically │ | |
| │ │ non-compliant. │ | |
| ├───────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Off-chain │ Delete from gateway DB. data_ref_id on-chain becomes a dangling pointer (returns "not found"). Actually deletable. │ | |
| └───────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| Scenario: Gateway outage during policy evaluation | |
| ┌───────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Model │ What Happens │ | |
| ├───────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Current │ Operators read encrypted data from on-chain, decrypt via KMS. Gateway only needed for the initial submitIdentity call. │ | |
| ├───────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Off-chain │ Operators need the gateway to fetch the encrypted envelope. If gateway is down, identity data is unavailable. │ | |
| └───────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| This last scenario is the strongest argument for on-chain storage. But consider: operators already need the gateway for task orchestration. If the | |
| gateway is down, no tasks are being evaluated anyway. So identity data availability is not the bottleneck. | |
| The Migration Path I'd Recommend | |
| Phase 1 (now): RSA-OAEP → on-chain (what exists today) | |
| Phase 2 (next): HPKE → off-chain data_ref_id (this migration) | |
| Phase 3 (later): data_ref_id on-chain pointer (contract upgrade) | |
| Phase 2 can be done without a contract change if the gateway accepts HPKE-encrypted identity data via newt_uploadEncryptedData and the gateway | |
| internally calls submitIdentity with the data_ref_id as the "data" string. The contract doesn't care what the string contains — it just stores | |
| bytes. The semantic meaning of the stored string changes from "encrypted blob" to "data reference ID", but the contract is unaware. | |
| Phase 3 (optional) adds a purpose-built contract function and event, but isn't strictly required for the migration. | |
| --- | |
| Next Batch of Questions | |
| 4. On the derived Ed25519 approach (Option A) — are you comfortable with the derivation scheme (secp256k1 sign a constant message → keccak256 → | |
| Ed25519 seed)? Or do you see concerns with binding the privacy key to the Turnkey wallet? The main trade-off: it means wallet migration (changing | |
| Identity Owner EOA) also rotates the Ed25519 key. | |
| 5. For the data_ref_id storage — should the gateway return the ref after upload and then newton-identity stores it on-chain via | |
| submitIdentity(owner, domain, dataRefId) (two-step, popup handles both), or should the gateway handle the on-chain storage internally like it does | |
| today (one RPC call, gateway submits the tx)? | |
| 6. Timeline/ordering — should this migration happen in newton-identity first (swap encryption, keep same gateway RPC), or does the gateway need | |
| changes first (new HPKE-based identity data handler)? What's the dependency order between this repo, newton-prover-avs, and newton-sdk? |
- Main Concerns with Changing from "Encrypted Blob" to "data_ref_id"
Three concerns worth thinking through:
Concern A: Backward compatibility during rollout
During the transition period, some identity data entries in the contract will contain RSA-OAEP encrypted blobs (old) and some will contain
data_ref_id hashes (new). Consumers reading identityData[owner][domain] need to distinguish between them.
Mitigation: The content hash will be a 64-char hex string (keccak256), while the RSA ciphertext is a much longer hex string (512+ chars for
2048-bit RSA). Length alone distinguishes them. Or add a prefix convention: ref:0xabc... vs raw ciphertext. The Phase 3 contract upgrade can
formalize this with a new storage slot or struct.
Concern B: Content hash collision (theoretical)
If data_ref_id = keccak256(envelope), two different users encrypting the same plaintext with different ephemeral HPKE keys will produce different
envelopes → different hashes. This is fine. But the same user re-encrypting the same data will also produce different hashes (different ephemeral
key each time), meaning re-uploads create new refs rather than deduplicating.
This is actually desirable — each upload is a distinct event, and you want the audit trail to show "user re-submitted identity data at time T"
rather than silently deduplicating.
Concern C: On-chain ref doesn't prove data integrity
With the current model, the on-chain encrypted blob IS the data — there's no way it can diverge from what's stored. With data_ref_id, the on-chain
hash is a claim that data with that hash exists in the gateway. If the gateway modifies or loses the data, the on-chain ref becomes a dangling
pointer.
Mitigation: Since data_ref_id = keccak256(envelope), the gateway (or any auditor) can verify integrity by re-hashing the stored envelope and
comparing to the on-chain ref. This is actually stronger than the current model — you get verifiable integrity checking. The content-addressed
design makes this self-enforcing.
Net assessment: None of these are blockers. The content-hash approach is sound.
- Atomicity Problem: Two-Step Upload
You're right to flag this. The two-step flow is:
Step 1: Popup → Gateway: uploadEncryptedData → returns data_ref_id
Step 2: Popup → Chain: submitIdentity(owner, domain, data_ref_id) → tx confirmed
If Step 1 succeeds but Step 2 fails (user rejects tx, gas issue, network error), you have orphaned data in the gateway.
Three approaches to handle this:
Approach A: TTL + Lazy Cleanup (simplest)
- All uploaded envelopes get a default TTL (e.g., 24 hours)
- When the on-chain tx confirms, the popup calls a second RPC: newt_confirmIdentityDataRef(data_ref_id) which removes the TTL (makes it permanent)
- Orphaned data expires naturally
- Con: Adds a third step. But this call can be fire-and-forget (non-blocking, best-effort)
Approach B: Gateway Confirms via Event Listener (best)
- Popup does Step 1 (upload) and Step 2 (on-chain tx) as today
- The gateway already watches for IdentityBound events (or will in Phase 3)
- When the gateway sees IdentityBound(owner, domain, data_ref_id) on-chain, it marks the stored envelope as confirmed
- Unconfirmed envelopes older than X hours get garbage collected
- Pro: No extra RPC call from the popup. The on-chain event IS the confirmation signal.
- Con: Requires the gateway to index IdentityBound events (but the chain watcher already exists for IdentityLinked/IdentityUnlinked)
Approach C: Optimistic with Retry (pragmatic)
- If Step 2 fails, the popup retries the tx
- If the user explicitly cancels, the popup calls a cleanup RPC
- Orphaned data is harmless — it's encrypted, has a content-hash ID, and takes minimal storage
- Pro: Simplest implementation
- Con: Relies on the popup staying open. If the user closes the popup after Step 1 but before Step 2, data is orphaned until GC
My recommendation: Approach B
The gateway watching for on-chain events is the cleanest separation of concerns. The popup's job ends after the on-chain tx is submitted. The
gateway handles its own data lifecycle.
Popup: upload envelope → get data_ref_id → submit tx → done
Gateway: see IdentityBound event → mark data_ref_id as confirmed
Gateway (cron): GC unconfirmed data_ref_ids older than 24h
- Dependency Order & Full Migration Plan
You confirmed: newton-prover-avs first → newton-identity → newton-sdk
Here's the full plan broken down by repo:
Phase 2a: newton-prover-avs (Gateway + Contract)
Gateway changes:
- New RPC method or extend existing: Accept HPKE-encrypted identity envelopes via newt_uploadEncryptedData with an identity_domain param to
distinguish identity data from general privacy data
- OR: New method newt_uploadIdentityEncrypted that wraps the same HPKE decrypt path but also calls submitIdentity with the data_ref_id - Identity data storage: Store the HPKE envelope in the same off-chain store as privacy data, keyed by data_ref_id = keccak256(envelope)
- Gateway event watcher: Watch for IdentityBound events to confirm data_ref_id storage. GC unconfirmed refs after 24h
- Backward compatibility: Keep newt_sendIdentityEncrypted working for RSA-OAEP during transition
- Decryption path: When operators need identity data, resolve data_ref_id → fetch envelope from DB → HPKE decrypt (instead of KMS decrypt from
on-chain blob)
Contract changes (Phase 3, can be deferred):
- New function: registerIdentityDataRef(owner, domain, dataRefId) — stores a 32-byte ref instead of arbitrary-length string
- New event: IdentityDataRefRegistered(owner, domain, dataRefId) — distinct from IdentityBound
- Deprecate: submitIdentity (mark as legacy, don't remove yet)
Phase 2b: newton-identity (This Repo)
Files to change:
┌─────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────┐
│ File │ Change │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/lib/kms.ts │ Replace RSA-OAEP with HPKE encryption (import from newton-sdk privacy module, or │
│ │ inline the createSecureEnvelope logic) │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/lib/turnkeyProvider.ts or new │ Add Ed25519 key derivation from Turnkey wallet (personal_sign → keccak256 → Ed25519 │
│ src/lib/privacyKey.ts │ seed) │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/app/(pages)/rpc/register-user-data/page.tsx │ Replace encryptSecret() → HPKE createSecureEnvelope(). Two-step: upload envelope, │
│ │ then submit data_ref_id on-chain │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/lib/identityRegistry.ts │ Add EncryptedIdentityDataRef EIP-712 type if we keep EIP-712 signing for the on-chain │
│ │ ref │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/utils/http.ts │ Add new RPC method constant for the HPKE upload endpoint │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ src/utils/encrypt.ts │ Delete (was already a duplicate, now fully superseded) │
├─────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────┤
│ Environment variables │ Replace NEXT_PUBLIC_KMS_PUBLIC_KEY (RSA) with gateway's X25519 public key (fetched at │
│ │ runtime via newt_getPrivacyPublicKey, or new env var) │
└─────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────┘
New dependencies:
- @hpke/core, @hpke/chacha20poly1305 — HPKE encryption (same as newton-sdk)
- @noble/curves — Ed25519 signing (same as newton-sdk)
- OR: Import directly from @magicnewton/newton-protocol-sdk if the privacy module is exported
Phase 2c: newton-sdk
- Update documentation to reflect that identity data now goes through the privacy layer
- Ensure uploadEncryptedData supports the identity domain parameter
- Update examples in docs/privacy/
Next Questions
-
For the HPKE encryption in newton-identity — should we import the privacy module functions directly from @magicnewton/newton-protocol-sdk
(reuse), or inline the HPKE logic in this repo (independence)? The SDK is already a dependency (^0.2.10), so importing is zero-cost, but it
couples the popup's encryption to the SDK's release cycle. -
For the Ed25519 key derivation — should the derivation message include the identityDomain (per-domain keys, stronger isolation) or just the
owner address (one key for all domains, simpler)? -
Should we write this migration plan to docs/PRIVACY_MIGRATION.md now so it's tracked in the repo, or wait until the gateway changes are specced
out more?
Cross-Repo Architecture Review
Here's how the three repos interact across the migration timeline:
Phase 1 (Current — what PR #140 targets)
newton-identity popup newton-prover-avs (Gateway + Contract)
RSA-OAEP encrypt (KMS key) newt_sendIdentityEncrypted RPC
EIP-712 sign (Turnkey) ──────► submitIdentity(owner, domain, blob)
Call gateway directly IdentityRegistry stores full blob on-chain
Operators KMS-decrypt at evaluation
newton-sdk (PR #140)
sendIdentityEncrypted() ← NOT used by anyone today
linkIdentity*() ← used by parent apps
unlinkIdentity*() ← used by parent apps / popup
identityDomainHash() ← used everywhere
Phase 2 (Post-migration — HPKE)
newton-identity popup newton-prover-avs (Gateway + Contract)
Derive Ed25519 from Turnkey newt_uploadEncryptedData RPC (off-chain)
HPKE encrypt (SDK privacy module) ──────► registerIdentityDataRef(owner, domain, refId)
Upload envelope via SDK IdentityRegistry stores 32-byte hash
Submit dataRefId on-chain Operators HPKE-decrypt at evaluation
newton-sdk
createSecureEnvelope() ← used by newton-identity
uploadEncryptedData() ← used by newton-identity
getPrivacyPublicKey() ← used by newton-identity
registerIdentityDataRef() ← NEW contract wrapper
linkIdentity*() ← unchanged
unlinkIdentity*() ← unchanged
identityDomainHash() ← unchanged
sendIdentityEncrypted() ← DEAD CODE, deprecated
The Problem with sendIdentityEncrypted in PR #140
┌─────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────┐
│ Concern │ Assessment │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┤
│ Who uses it today? │ Nobody. newton-identity calls the gateway directly. │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┤
│ Who uses it │ Nobody. It's replaced by uploadEncryptedData + registerIdentityDataRef. │
│ post-migration? │ │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┤
│ Is it │ No. It wraps the newt_sendIdentityEncrypted RPC which stores full blobs on-chain. │
│ migration-safe? │ Post-migration, this RPC is deprecated in favor of newt_uploadEncryptedData (off-chain) + │
│ │ registerIdentityDataRef (on-chain hash). │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┤
│ API surface cost │ Adds a function that must be removed/deprecated in Phase 2. │
└─────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────┘
The migration plan (Phase 2c) explicitly says the SDK's role for identity data is through the privacy module
(createSecureEnvelope, uploadEncryptedData, getPrivacyPublicKey) — not through sendIdentityEncrypted.
What Survives the Migration (Keep in PR #140)
┌─────────────────────────────┬──────────────────┬──────────────────────────────────────────────────────┐
│ Function │ Migration-proof? │ Why │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ linkIdentity │ Yes │ Migration doc: "Identity linking is unaffected" │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ linkIdentityAsSigner │ Yes │ Same │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ linkIdentityAsSignerAndUser │ Yes │ Same │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ linkIdentityAsUser │ Yes │ Same │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ unlinkIdentityAsSigner │ Yes │ Same │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ unlinkIdentityAsUser │ Yes │ Same │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ identityDomainHash │ Yes │ Pure utility, used in both phases │
├─────────────────────────────┼──────────────────┼──────────────────────────────────────────────────────┤
│ sendIdentityEncrypted │ No │ Replaced by privacy module + registerIdentityDataRef │
└─────────────────────────────┴──────────────────┴──────────────────────────────────────────────────────┘
Recommendation for PR #140
Remove sendIdentityEncrypted from PR #140. Keep everything else.
Rationale:
- It's not used by any consumer today (newton-identity calls gateway directly)
- It wraps an RPC that will be deprecated post-migration
- Adding it creates API surface that must be removed/deprecated — churn with no value
- The migration plan already specifies the replacement path (privacy module exports)
What to add instead (forward-looking):
- Nothing new for data submission — that comes via the existing privacy module post-migration
- Consider adding registerIdentityDataRef as a writeContract wrapper when the contract changes land in newton-prover-avs
(not now — the contract doesn't have this function yet)
Do you want me to:
- Update PR #140 — remove sendIdentityEncrypted, keep link/unlink + identityDomainHash, update docs/tests accordingly
- Discuss further — if you see a reason to keep sendIdentityEncrypted (e.g., other consumers, Phase 1 completeness)
- Something else — different scoping
In the current connect flow, ConnectKycCard.tsx does:
- User completes Persona KYC
- Component encrypts KYC data (RSA-OAEP)
- Component EIP-712 signs it
- Component calls newt_sendIdentityEncrypted → gateway calls submitIdentity on-chain
With the new model, this becomes:
- User completes Persona KYC
- Component encrypts KYC data (RSA-OAEP for now, HPKE later)
- Component calls newt_uploadIdentityEncrypted → gateway stores off-chain, returns data_ref_id
- Component calls registerIdentityData(domain, dataRefId) on-chain → popup sends the tx directly via Turnkey wallet
The popup already has the Turnkey wallet set up (the user just authenticated). And the popup already sends on-chain transactions — the unlink flow
(unlinkIdentityAsSigner) does exactly this via the Turnkey provider adapter in useTurnkeyAuth.ts.
So the pattern already exists. The change in ConnectKycCard.tsx would be:
Before: encrypt → sign → gateway RPC (gateway does on-chain tx)
After: encrypt → gateway RPC (off-chain store) → popup does on-chain tx
The popup gets back a data_ref_id from the gateway, then sends a transaction to IdentityRegistry.registerIdentityData(domain, dataRefId) using the
same Turnkey wallet client pattern as the unlink flow. The user signs one extra transaction (registering their data ref) — but this is a one-time
operation during initial KYC.
This also means we can remove the EIP-712 EncryptedIdentityData signing entirely — it was only needed because the gateway was submitting on behalf
of the user (needed proof of consent). Now the user submits directly, so msg.sender proves consent.
Shall I proceed with the implementation? The execution order would be:
- Contract: Remove submitIdentity, add registerIdentityData (identity owner calls directly)
- Gateway: New newt_uploadIdentityEncrypted RPC (off-chain store, returns data_ref_id)
- Bindings: make generate-bindings
- Gateway types + handler: Wire up the new RPC
❯ so how does the gateway ensure that the registerIdentityData indeed gets call and succeeds by the user? also, what is the difference between
newt_uploadIdentityEncrypted and newt_uploadEncryptedData now?
⏺ Great questions. Let me address both:
- How does the gateway ensure registerIdentityData gets called?
It doesn't need to guarantee it — this is the same atomicity pattern we decided on earlier (Approach B: event-based confirmation):
- Gateway stores encrypted data off-chain with a "pending" status
- Gateway's chain watcher listens for IdentityBound events
- When it sees IdentityBound(owner, domain, dataRefId) on-chain, it marks the ref as confirmed
- Unconfirmed refs older than 24h get garbage collected
If the user never calls registerIdentityData (closes popup, rejects tx, gas failure), the orphaned data expires. The encrypted data is useless
without the on-chain ref anyway — nobody will look it up.
This is no different from general privacy data: uploadEncryptedData stores envelopes that might never be referenced in a createTask call. Orphaned
data is a non-issue.
- newt_uploadIdentityEncrypted vs newt_uploadEncryptedData — do we need both?
Thinking through this more carefully, the differences are thin:
┌────────────┬──────────────────────────────────────────────┬───────────────────────────────────────────────────┐
│ │ newt_uploadEncryptedData (existing) │ newt_uploadIdentityEncrypted (proposed) │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Input │ SecureEnvelope + Ed25519 sig + sender_pubkey │ Encrypted blob + identity_owner + identity_domain │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Auth │ Ed25519 signature verification │ EIP-712 signature (for now) or just API key │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Storage │ Postgres (encrypted_data_refs table) │ Same Postgres (same or similar table) │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Returns │ UUID data_ref_id │ keccak256 content hash data_ref_id │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ On-chain │ Nothing — consumer uses ref in createTask │ Nothing — consumer calls registerIdentityData │
├────────────┼──────────────────────────────────────────────┼───────────────────────────────────────────────────┤
│ Encryption │ HPKE (SecureEnvelope) │ RSA-OAEP now, HPKE later │
└────────────┴──────────────────────────────────────────────┴───────────────────────────────────────────────────┘
The core difference right now is encryption scheme (RSA vs HPKE) and auth method (Ed25519 vs EIP-712/API key). Once we migrate to HPKE, the only
remaining difference is the identity_domain field and the fact that identity data gets referenced on-chain via registerIdentityData instead of in
createTask.
My revised recommendation: Keep them as one RPC method — newt_uploadEncryptedData — but add an optional identity_domain field. When present, the
gateway tags the stored data as identity data (for GC policy, audit). The data_ref_id return value works the same either way.
This means:
- No new RPC method to maintain
- When HPKE migration lands, identity uploads are identical to privacy uploads
- The only distinction is the optional identity_domain metadata
But there's a wrinkle: right now identity data is RSA-OAEP encrypted, not HPKE. The existing uploadEncryptedData expects a SecureEnvelope with
Ed25519 sig. We'd need to either:
(A) Create newt_uploadIdentityEncrypted as a temporary stopgap that accepts RSA ciphertext, then delete it when HPKE migration happens (more code
now, less later)
(B) Extend newt_uploadEncryptedData to accept both formats (adds complexity to one handler)
(C) Ship the HPKE client-side encryption in newton-identity NOW as part of this work, and use the existing uploadEncryptedData as-is (more scope
but cleanest result)
Given that you want to ship fast first, Option A makes the most sense — a simple stopgap RPC that stores KMS-encrypted data off-chain and returns
the content hash. It's small, self-contained, and gets deleted entirely when HPKE lands.
What's your preference: A (temporary stopgap RPC), B (extend existing), or C (do HPKE client-side now)?
Here's the full architecture:
End-to-End Flow
Newton Identity (popup) Newton Gateway Ethereum (Sepolia)
| | |
| 1. POST newt_uploadIdentityEncrypted | |
| { encrypted_data, identity_owner, | |
| identity_domain, chain_id, | |
| identity_owner_sig (EIP-712) } | |
|--------------------------------------->| |
| | 2. Verify EIP-712 sig |
| | 3. Compute data_ref_id = |
| | keccak256(encrypted_data) |
| | 4. Store in PostgreSQL |
| | (identity_data_refs table, |
| | confirmed_at = NULL, |
| | expires_at = now + 1h) |
| 5. Return { data_ref_id } | |
|<---------------------------------------| |
| |
| 6. Send tx: registerIdentityData(domain, data_ref_id) |
| (via Turnkey wallet, user signs) |
|------------------------------------------------------------------------------->|
| |
| | 7. IdentityBound event emitted |
| | (owner, domain, data_ref_id) |
| | |
| | 8. Gateway background watcher |
| | (every 60s, polls IdentityBound) |
| | UPDATE identity_data_refs |
| | SET confirmed_at = NOW() |
| | WHERE data_ref_id = '0x...' |
| | |
| | 9. GC: DELETE FROM identity_data_refs |
| | WHERE expires_at < NOW() |
| | AND confirmed_at IS NULL |
Components
┌──────────────────────┬──────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┐
│ Component │ What it does │ Where │
├──────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ Newton Identity │ Encrypts data, uploads to gateway, sends on-chain tx │ Browser (this repo) │
│ popup │ │ │
├──────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ Gateway RPC handler │ newt_uploadIdentityEncrypted — validates EIP-712 sig, │ crates/gateway/src/rpc/api/identity.rs │
│ │ stores in DB, returns data_ref_id │ │
├──────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ PostgreSQL │ identity_data_refs table — stores encrypted data blobs │ Gateway's existing Postgres (same DB as │
│ │ keyed by content hash │ encrypted_data_refs, task state, API keys) │
├──────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ IdentityRegistry │ registerIdentityData(domain, dataRefId) — stores the │ On-chain (Sepolia/Mainnet) │
│ contract │ 32-byte ref on-chain, emits IdentityBound │ │
├──────────────────────┼──────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ Gateway background │ Polls IdentityBound events every 60s, marks matching │ crates/gateway/src/handler/mod.rs │
│ watcher │ refs as confirmed, GCs expired orphans │ (start_identity_data_ref_watcher) │
└──────────────────────┴──────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┘
The DB
It's the gateway's PostgreSQL database — the same Postgres instance that already stores:
- encrypted_data_refs (privacy layer HPKE envelopes)
- api_keys (API key management)
- Task state and other gateway operational data
The new identity_data_refs table sits alongside these. Migration: crates/gateway/migrations/20260317100000_create_identity_data_refs.sql.
Who does the confirmation polling?
The gateway itself — not the chain watcher. It's a tokio::spawn background task inside GatewayServiceImpl, started alongside the existing
start_cleanup_task in crates/gateway/src/lib.rs. The chain watcher binary (newton-chain-watcher) is a separate process designed for task/operator
events and uses Redis — overkill for this simple confirmation loop.
Data lifecycle
Upload → DB row created (confirmed_at = NULL, expires_at = +1h)
On-chain tx → IdentityBound event emitted
Watcher → DB row updated (confirmed_at = NOW(), expires_at preserved)
GC → Rows with expires_at < NOW() AND confirmed_at IS NULL get deleted
Confirmed rows live forever (no expiry). Only orphaned, never-registered refs get cleaned up after 1 hour.
❯ one last question: multichain related question: is Identity Data single chain bound or multichain? does newton-identity calls
registerIdentityData only on the source chain or does it call per individual chain (src and destination chains?) tldr: is the identity data chain
agnostic or is it chain-specific?
⏺ Great question. Let me check what the current code does:
⏺ Searched for 4 patterns (ctrl+o to expand)
⏺ Key finding from line 56: avs.identity_registry = src_avs.identity_registry — the IdentityRegistry address is loaded from the source chain config
only. It's not deployed per-destination chain.
And from fetch_identity_data: operators read identity data from identity_registry.identityData(owner, domain) — this is a single contract call to
a single chain's IdentityRegistry.
Here's the answer:
Currently: Identity data is single-chain (source chain only).
The IdentityRegistry contract is deployed only on the source chain (Sepolia/Ethereum). The config loads it from src_avs.identity_registry.
Operators on any chain read identity data from this single source-chain contract.
This is actually the right design — and it becomes even better with the data_ref_id model:
┌─────────────┬───────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────┐
│ Aspect │ Old (on-chain blob) │ New (data_ref_id) │
├─────────────┼───────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ Contract │ IdentityRegistry on source chain stores full │ IdentityRegistry on source chain stores 32-byte data_ref_id │
│ │ encrypted blob │ │
├─────────────┼───────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ Data access │ Operators read blob from source chain, decrypt │ Operators read data_ref_id from source chain, fetch from gateway DB, │
│ │ │ decrypt │
├─────────────┼───────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ Multi-chain │ Destination chain operators must cross-chain read to │ Same — but reading 32 bytes is cheaper than 512+ bytes cross-chain │
│ │ source │ │
└─────────────┴───────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────┘
The encrypted data itself is chain-agnostic — it lives in the gateway's PostgreSQL, not on any chain. The data_ref_id is a content hash that works
the same regardless of which chain it's registered on.
newton-identity calls registerIdentityData on the source chain only. The chainId from sessionStorage determines which chain, but in practice it's
always the source chain where IdentityRegistry is deployed.
Future multi-chain optimization: If IdentityRegistry is deployed on multiple chains (e.g., source + destination), the same data_ref_id could be
registered on both chains without re-uploading the data. One gateway DB upload, N chain registrations. But that's a future optimization — right
now single source chain is correct.
Want me to save this context and move on to anything else, or should we wrap up this session?
✻ Cooked for 39s
❯ I think identityregistry contract is already deployed to all chains, source and destination chains? Can you double check?
⏺ Searched for 1 pattern (ctrl+o to expand)
⏺ Line 56 shows identity_registry is loaded from src_avs only. But let me check the deployment data to see if it's actually deployed on destination
chains too:
⏺ Searched for 1 pattern (ctrl+o to expand)
⏺ Core deployments exist on multiple chains (31337, 11155111, 84532, 1). Let me check if identityRegistry is in these deployment JSONs:
⏺ Bash(for f in /Users/dennis.won/projects/newton-prover-avs/contracts/script/deployments/core/*.json; do echo "=== $(basename $f) ===" && python3
-c "import json; d=j…)
⎿ === 1-prod.json ===
identityRegistry: NOT FOUND
=== 1-stagef.json ===
… +13 lines (ctrl+o to expand)
⏺ Not in core. Let me check the newton-prover deployment JSONs:
⏺ Bash(for f in /Users/dennis.won/projects/newton-prover-avs/contracts/script/deployments/newton-prover/*.json; do echo "=== $(basename $f) ===" &&
python3 -c "import…)
⎿ === 1-prod.json ===
identityRegistry: NOT FOUND
=== 1-stagef.json ===
… +11 lines (ctrl+o to expand)
⏺ You're partially right — IdentityRegistry is deployed on:
- Sepolia (11155111): Both stagef and prod
- Local (31337): stagef
- NOT on Base Sepolia (84532) or Mainnet (1)
So currently it's source chain only (Sepolia is the EigenLayer source chain). It's NOT deployed to destination chains like Base Sepolia.
However, let me check the deploy scripts to see if there's intent to deploy it on destination chains:
⏺ Searched for 3 patterns (ctrl+o to expand)
⏺ You're right — IdentityRegistry is deployed on both source and destination chains. The NewtonCrossChainDeploymentLib.sol deploys it on destination
chains too (line 589-598). It just hasn't been deployed to Base Sepolia/Mainnet yet (those deployment JSONs are missing the address), but the
deploy scripts are ready for it.
This changes the picture:
Updated Architecture: Identity Data is Multi-Chain
Gateway PostgreSQL
┌──────────────────┐
│ identity_data_refs│
│ (encrypted data) │
│ data_ref_id = 0x… │
└────────┬─────────┘
│
fetch by data_ref_id
│
┌────────────────────────┼────────────────────────┐
│ │ │
Sepolia (source) Base Sepolia (dest) Mainnet (dest)
IdentityRegistry IdentityRegistry IdentityRegistry
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│identityData │ │identityData │ │identityData │
│[owner][domain]│ │[owner][domain]│ │[owner][domain]│
│= "0xabc..." │ │= "0xabc..." │ │= "0xabc..." │
└──────────────┘ └──────────────┘ └──────────────┘
same data_ref_id registered on each chain
The data_ref_id model is perfect for this. The identity owner:
- Uploads encrypted data once → gateway stores it → returns data_ref_id
- Calls registerIdentityData(domain, dataRefId) on each chain where they want their identity usable
The encrypted data is stored once in the gateway DB. The data_ref_id (content hash) is the same across all chains. Operators on any chain read the
ref from their local IdentityRegistry and fetch the data from the gateway.
Implications for our implementation:
- Gateway watcher needs to poll IdentityBound events from all chains, not just self.chain_id. Let me check if the current watcher handles this
correctly — it only polls one chain right now. - newton-identity popup may need to register the data_ref_id on multiple chains, or the SDK handles this.
Should I update the watcher to poll all configured chains, or is that a follow-up? The current single-chain polling will work for the Sepolia-only
case but won't confirm refs registered on destination chains.
✻ Sautéed for 1m 25s
❯ so does that mean we will have NOT the gateway but the chain watcher to poll the onchain event for IdentityBound?
⏺ Yes — you're right. The chain watcher is already deployed per-chain and watches events on each chain independently. It's the correct component for
this.
The current architecture:
- Chain watcher: Runs per-chain, watches on-chain events, relays to gateway via HTTP
- Gateway: Receives relayed events, processes them
So the flow should be:
Chain Watcher (Sepolia) ──► watches IdentityBound ──► relays to Gateway ──► confirms data_ref_id
Chain Watcher (Base Sepolia) ──► watches IdentityBound ──► relays to Gateway ──► confirms data_ref_id
Chain Watcher (Mainnet) ──► watches IdentityBound ──► relays to Gateway ──► confirms data_ref_id
This means I should:
- Remove the gateway background watcher I just added (it's the wrong component)
- Add IdentityBound event type to the chain watcher's ChainEventType enum
- Add IdentityBound event filtering in the chain watcher listener
- Add a handler in the gateway that receives relayed IdentityBound events and confirms the data_ref_id in the DB
Let me revert the gateway watcher and move the logic to the chain watcher. Let me first check how the chain watcher relays events to the gateway:
⏺ Read 2 files (ctrl+o to expand)
⏺ Now I have the full picture. The pattern is clear:
- Chain watcher listens via WebSocket, emits ChainEvent through an mpsc channel
- GatewayClient relays events to gateway's /watcher HTTP endpoint
- Gateway processes the event (existing handler in handler/mod.rs)
So the correct implementation is:
- Revert the gateway background watcher (wrong component)
- Add IdentityBound to ChainEventType enum
- Add IdentityBound event subscription in the chain watcher listener
- Add handler in gateway's /watcher endpoint to confirm data_ref_id on receiving the event
But this is a significant scope expansion — the chain watcher needs identity_registry_address config, the listener needs a new event subscription,
and the gateway needs a new event handler. This is a separate PR's worth of work.
For the current PR, I should:
- Revert the gateway watcher from the current commit
- Keep the IdentityDataRefRepository.confirm() and gc_expired() methods (they'll be used by the chain watcher handler)
- Add a TODO/follow-up for the chain watcher integration
The GC can still run as a simple gateway periodic task (just the DB cleanup, no chain polling) — that's lightweight and appropriate for the
gateway. The event-based confirmation is what belongs in the chain watcher.
Should I:
- (A) Revert the gateway watcher entirely from this PR, track chain watcher integration as a follow-up
- (B) Keep a minimal gateway periodic task for GC only (no event polling), add chain watcher integration as a follow-up
- (C) Implement the chain watcher integration now (bigger scope, same PR)
gateway-assigned)? Content hash gives you dedup and verifiability. UUID gives the gateway more control over storage lifecycle. --> data_ref_id be
a content hash of the envelope (content-addressed, deterministic) would be better. 2. Option A: Derive Ed25519 key from Turnkey wallet sounds the
best, at the moment at least. 3. This sounds good: "The Migration Path I'd Recommend
Phase 1 (now): RSA-OAEP → on-chain (what exists today)
Phase 2 (next): HPKE → off-chain data_ref_id (this migration)
Phase 3 (later): data_ref_id on-chain pointer (contract upgrade)
Phase 2 can be done without a contract change if the gateway accepts HPKE-encrypted identity data via newt_uploadEncryptedData and the gateway
internally calls submitIdentity with the data_ref_id as the "data" string. The contract doesn't care what the string contains — it just stores
bytes. The semantic meaning of the stored string changes from "encrypted blob" to "data reference ID", but the contract is unaware.
Phase 3 (optional) adds a purpose-built contract function and event, but isn't strictly required for the migration." we should do option 3 as
well, change from "encrypted blob" to "data reference ID". 4. changes from "encrypted blob" to "data reference ID" --> this sounds reasonable, but
what are your main concerns with this approach? 5. "For the data_ref_id storage — should the gateway return the ref after upload and then
newton-identity stores it on-chain via submitIdentity(owner, domain, dataRefId) (two-step, popup handles both), or should the gateway handle the
on-chain storage internally like it does today (one RPC call, gateway submits the tx)?" --> the former sounds fovorable in terms of scalability
because if we have gateway to handles, then we would need to pay all the gas fee for tx, and would also hit nonce issue potentially. But the
concern is atomicity, because after data ref is saved, and if the data ref is not actually set onchain, then we have dangling stale data. How
should we handle this problem? 6. we should work on newton-prover-avs first, and then newton-identity and then newton-sdk