-
-
Save denniswon/28aa29640e8e9eb6677a6e175a013834 to your computer and use it in GitHub Desktop.
| Here's the full picture: | |
| --- | |
| Newton Privacy Layer — Known Limitations & Next Steps | |
| ★ Insight ───────────────────────────────────── | |
| The limitations fall into three buckets: (1) security gaps where the system doesn't enforce what it should, (2) trust assumptions | |
| where centralization remains, and (3) missing infrastructure that blocks future phases. Each has a clear resolution path — none are | |
| architectural dead ends. | |
| ───────────────────────────────────────────────── | |
| Known Limitations | |
| 1. Challenger Cannot Verify Privacy Tasks (NEWT-664) — SECURITY GAP | |
| Problem: evaluate_and_prove() in crates/challenger/src/lib.rs:171 replays task responses to detect misbehavior, but it has no access | |
| to decrypted privacy data. The Rego evaluation runs without data.identity.*, data.privacy.*, or data.ephemeral.* — so the challenger's | |
| result will differ from the operator's result even when the operator was honest. | |
| Impact: Operators can return arbitrary results for privacy-enabled tasks without risk of slashing. This is the single largest security | |
| gap in the shipped system. | |
| Solution (two-stage): | |
| - Phase 1.5 (short-term): Detect privacy-enabled tasks and skip challenge evaluation + emit challenger_privacy_tasks_skipped metric at | |
| WARN level. Prevents false-positive slashing. | |
| - Phase 2 (long-term): Challenger holds a FROST key share from the DKG ceremony, requests partial decryptions from operators, combines | |
| t-of-n shares, and replays with full privacy context. | |
| Linear: NEWT-664, High priority, Backlog. | |
| --- | |
| 2. Gateway Reconstructs Plaintext (Trust Assumption) | |
| Problem: Even with threshold DKG, the gateway is the entity that combines the partial DH outputs via Lagrange interpolation. It sees | |
| the reconstructed shared secret and decrypts the HPKE ciphertext. This is true for both centralized and threshold modes — the | |
| difference is whether the gateway holds the full key (centralized) or reconstructs it per-task from operator contributions | |
| (threshold). | |
| Impact: A compromised gateway can read all ephemeral privacy data. Identity and confidential data are safer — operators decrypt those | |
| locally. | |
| Solution: Phase 3 MPC — operators evaluate the Rego policy directly on encrypted data without any party seeing plaintext. This | |
| requires: | |
| - NEWT-173 (privacy-preserving policy evaluation with MPC/ZK) | |
| - NEWT-168 (MPC framework — canceled, needs rescoping) | |
| - Possibly FHE or garbled circuits depending on benchmarking (NEWT-631) | |
| Timeline: Phase 3, no concrete date. | |
| --- | |
| 3. No Epoch-Based Key Rotation (Forward Secrecy Gap) | |
| Problem: The FROST DKG ceremony produces a single set of key shares that remain valid indefinitely. If an attacker compromises t | |
| shares across any time window, they can decrypt all data ever encrypted to that key. | |
| Impact: No forward secrecy. A single key compromise has unlimited blast radius. | |
| Solution: PSS (Proactive Secret Sharing) with epoch rotation: | |
| - NEWT-628 (canonical spec, due 2026-05-30): epoch-based rotation, MSK destruction, grace periods | |
| - NEWT-639: EpochRegistry Solidity contract for on-chain epoch lifecycle | |
| - Old shares zeroed via zeroize crate after 2-epoch grace period | |
| - MPK stays constant across epochs (no client re-encryption needed) | |
| Timeline: Phase 2C, target Q2 2026. | |
| --- | |
| 4. No Privacy-Specific Slashing | |
| Problem: Operators who leak plaintext, submit invalid partial decryptions, or miss DKG ceremony rounds face no economic penalty beyond | |
| the general task misbehavior slashing. | |
| Impact: No economic deterrent for privacy-specific misbehavior. | |
| Solution: | |
| - NEWT-629 (slashing conditions, blocked by NEWT-628 epoch context) | |
| - NEWT-640 (PrivacySlasher contract) | |
| - NEWT-641 (DleqVerifier Solidity library for on-chain DLEQ proof verification) | |
| - Slashing conditions: missed partial decryption, invalid DLEQ proof, plaintext leakage attestation | |
| Timeline: Phase 2D, after epoch rotation ships. | |
| --- | |
| 5. No On-Chain Auditability for Threshold Operations | |
| Problem: Threshold decryption happens entirely off-chain. There's no on-chain record of which operators participated, which DLEQ | |
| proofs were submitted, or which epoch's key was used. | |
| Impact: No verifiable audit trail for privacy operations. Disputes rely on off-chain logs. | |
| Solution: NEWT-630 (on-chain aggregation commitments) — post a commitment hash of the threshold operation metadata on-chain after each | |
| decryption. | |
| Timeline: Phase 2C, alongside epoch rotation. | |
| --- | |
| 6. Plaintext in Gateway Memory Not Wrapped with secrecy Crate | |
| Problem: Decrypted ephemeral privacy data exists as plain Vec<u8> / String in gateway memory during the task lifecycle. If the process | |
| dumps core or a debugging tool attaches, plaintext is readable. | |
| Impact: Low — requires host-level access to exploit. But defense-in-depth says wrap it. | |
| Solution: Wrap with secrecy::Secret<Vec<u8>> and zeroize-on-drop. The keystore already uses zeroize::Zeroizing (confirmed in | |
| crates/core/src/dkg/keystore.rs:28). Extend this pattern to ephemeral decrypted data in inline_privacy.rs and sync.rs. | |
| Timeline: Quick win, can be done anytime. | |
| --- | |
| 7. Private Data Storage Still on PostgreSQL | |
| Problem: Operators currently access encrypted data blobs from the gateway's PostgreSQL database. External operators (not co-located | |
| with the gateway) need direct DB access — a security and operational risk. | |
| Impact: Limits operator decentralization. External operators can't participate in identity/confidential data flows without DB | |
| credentials. | |
| Solution: Private Data Storage Layer (decided 2026-03-19 team meeting): | |
| - Phase 1 (done): Postgres stopgap with operator DB access | |
| - Phase 2: HPKE migration (done) | |
| - Phase 3: redb (pure Rust) + Jellyfish Merkle Tree, NATS JetStream sync | |
| - Linear: NEWT-799 through NEWT-813 | |
| Timeline: Separate project track, Q3 2026. | |
| --- | |
| Prioritized Next Steps | |
| ┌──────────┬─────────────────────────────────────────────┬──────────────┬──────────┬────────────────────────────────────────────┐ | |
| │ Priority │ What │ Linear │ Effort │ Why First │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 1 │ Challenger privacy skip (NEWT-664 Phase │ NEWT-664 │ 1-2 days │ Security gap — false-positive slashing │ | |
| │ │ 1.5) │ │ │ risk │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 2 │ secrecy crate for ephemeral plaintext │ (no ticket) │ 1 day │ Quick defense-in-depth win │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 3 │ PSS epoch rotation (NEWT-628) │ NEWT-628 │ 3-4 │ Forward secrecy — longest-pole Phase 2 │ | |
| │ │ │ │ weeks │ item │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 4 │ EpochRegistry contract (NEWT-639) │ NEWT-639 │ 1-2 │ On-chain epoch lifecycle, blocks slashing │ | |
| │ │ │ │ weeks │ │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 5 │ On-chain aggregation commitments (NEWT-630) │ NEWT-630 │ 1 week │ Auditability for threshold ops │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 6 │ PrivacySlasher + DleqVerifier │ NEWT-640, │ 2 weeks │ Economic security for privacy misbehavior │ | |
| │ │ (NEWT-640/641) │ 641 │ │ │ | |
| ├──────────┼─────────────────────────────────────────────┼──────────────┼──────────┼────────────────────────────────────────────┤ | |
| │ 7 │ Privacy slashing conditions (NEWT-629) │ NEWT-629 │ 1 week │ Ties slasher to specific violations │ | |
| └──────────┴─────────────────────────────────────────────┴──────────────┴──────────┴────────────────────────────────────────────┘ | |
| Items 1-2 are quick wins. Items 3-7 form the Phase 2C/2D roadmap and should be executed in order (628 → 639 → 630 → 640/641 → 629). | |
| --- | |
| ★ Insight ───────────────────────────────────── | |
| The system is in a solid position for a pre-mainnet AVS — all three privacy data paths work end-to-end, threshold DKG is operational, | |
| and KMS is fully removed. The remaining gaps are about hardening (forward secrecy, slashing, auditability) rather than fundamental | |
| capability. NEWT-664 is the only gap that affects correctness of the current system — everything else is about reducing trust | |
| assumptions or adding economic security. The Phase 2C/2D work is pure Solidity + coordination protocol — the Rust-side crypto is done. | |
| ───────────────────────────────────────────────── | |
| Want me to create a Linear ticket for the secrecy crate wrapping, or start working on NEWT-664 (challenger privacy skip)? |
looks right
⏺ Question 4 of N: Where does the challenger get the raw DLEQ proofs (private inputs)?
The on-chain commitment (NEWT-630) stores only a hash. The challenger needs the actual per-operator data to feed into the SP1 circuit.
Three options:
A) EigenDA: Gateway posts the raw DLEQ data to EigenDA alongside the on-chain commitment hash. EigenDA provides data availability
guarantees — the data is retrievable by anyone for the dispute window. Cost: ~$0.01 per blob. This is how EigenLayer-native systems
typically handle DA.
B) Gateway API endpoint: Gateway exposes a newt_getDecryptionProofs(taskId) RPC endpoint that returns the raw data. Simpler but relies on
the gateway being online and honest about serving the data. A compromised gateway could refuse to serve proofs for tasks where it
colluded.
C) Operator-direct: Challenger contacts each operator directly (via their socket address) to re-request their partial decryption + DLEQ
proof. Operators are incentivized to respond (if they're honest, they want the bad operator slashed). No single point of failure.
My recommendation: Option A (EigenDA). Reasons:
- The gateway is already submitting BLS data on-chain — adding a DA blob is a small incremental cost
- Removes the gateway as a trust dependency for data retrieval (matches our "trustless" goal)
- The challenger doesn't need to contact operators individually (they might be unreachable)
- EigenDA is the natural DA layer for an EigenLayer AVS
- If EigenDA is unavailable, Option C (operator-direct) serves as a fallback
A, B, or C?
❯ Agreed. EigenDA
⏺ Question 5 of N: Should the SP1 DLEQ circuit be a new circuit or extend the existing sp1-rego circuit?
Currently we have one SP1 circuit at circuits/sp1-rego/ that verifies Rego policy evaluation for the existing challenger. Two options:
A) New separate circuit (circuits/sp1-dleq/): Independent binary, independent proof key, independent verifier on-chain. Clean separation
— DLEQ verification has nothing to do with Rego policy evaluation.
B) Extend sp1-rego: Add a "mode" flag — the circuit either verifies Rego policy OR verifies DLEQ proofs depending on the input. Shares
the same SP1 verifier contract on-chain.
My recommendation: Option A (separate circuit). The two proofs verify completely different things — Rego policy correctness vs DLEQ
cryptographic validity. Combining them into one circuit adds complexity for no benefit. The SP1 verifier contract on-chain can verify
proofs from any circuit (it's parameterized by vkey), so there's no deployment overhead from having two circuits.
A or B?
❯ a
⏺ Question 6 of N: How does the challenger know something is wrong in the first place?
The challenger needs a trigger to investigate a task's DLEQ proofs. It won't speculatively verify every task's proofs via SP1 (too
expensive). Three detection models:
A) Verify all — optimistic: The challenger fetches DLEQ data from EigenDA for every privacy-enabled task and runs off-chain DLEQ
verification (cheap Rust code, no SP1). Only when it detects an invalid proof does it generate the expensive SP1 proof for on-chain
submission. This is the same pattern as the existing policy challenger — it re-evaluates every task, only challenges when results differ.
B) Selective — commitment-based: The challenger only investigates tasks where the gateway's on-chain commitment includes an operator that
was previously flagged as suspicious (reputation system). Reduces work but could miss first-time attackers.
C) Reactive — external report: Anyone can tip the challenger (via an RPC endpoint or on-chain bounty) that a specific task has bad
proofs. The challenger then investigates that specific task.
My recommendation: Option A (verify all, optimistic). Reasons:
- Off-chain Ed25519 DLEQ verification is ~microseconds per proof — trivially cheap for the challenger to verify every privacy task
- Matches the existing challenger architecture: verify everything off-chain, only go on-chain when a violation is found
- No reputation system or tipping infrastructure needed
- The SP1 proof generation (expensive part) only happens on the rare slashing path
The challenger flow becomes:
- Watch EpochRegistered and respondToTask events
- For each privacy-enabled task: fetch DLEQ data from EigenDA
- Run dleq::verify() in Rust for each operator's proof (~microseconds)
- If any fail: generate SP1 batch proof and submit to PrivacySlasher
A, B, or C?
❯ option A
⏺ Question 7 of N: Timing — when does the commitment get posted?
The DLEQ proofs happen during the Prepare phase, but the on-chain transaction happens at respondToTask (end of Commit phase). The
commitment needs to be included in the on-chain submission. Two options:
A) Embed in respondToTask: Add decryptionCommitment and operatorBitmap as new fields in TaskResponseMetadata. The commitment travels with
the existing BLS-aggregated response — one transaction, no extra on-chain call. The gateway computes the commitment during the Prepare
phase and carries it through to submission.
B) Separate transaction: Gateway submits registerDecryptionCommitment(taskId, commitment, bitmap) as a separate call after respondToTask.
Two transactions per privacy task. More gas, but doesn't require modifying the TaskManager interface.
My recommendation: Option A (embed in respondToTask). Modifying TaskResponseMetadata is a struct change that affects the ABI selector
(per lessons.md — "struct changes silently break all callers"), but NEWT-630 is a planned breaking change anyway. Embedding avoids the
extra transaction cost (~21k gas saved per task) and ensures the commitment is atomically bound to the task response — no window where a
response exists without a commitment.
The trade-off: changing TaskResponseMetadata means all clients (gateway, challenger) must update their ABI. But this is a coordinated
deploy — same as any contract upgrade.
A or B?
❯ option A
⏺ Question 8 of N: What happens for non-privacy tasks?
Not every task uses threshold decryption. For tasks without privacy data, there are no DLEQ proofs to commit. The decryptionCommitment
field in TaskResponseMetadata needs to handle this.
A) Zero-value sentinel: decryptionCommitment = bytes32(0) and operatorBitmap = 0 for non-privacy tasks. The PrivacySlasher rejects
challenges against tasks with zero commitment. Simple, no branching in the contract.
B) Boolean flag: Add bool hasDecryptionCommitment to TaskResponseMetadata. Explicit but uses an extra storage slot.
C) Omit from non-privacy tasks: Use a separate respondToTaskWithPrivacy() function that includes the commitment fields. Non-privacy tasks
use the existing respondToTask(). Two code paths on both the gateway and contract side.
My recommendation: Option A (zero-value sentinel). Reasons:
- bytes32(0) is the natural default for unset storage — no extra logic needed
- One function signature for all tasks — the gateway always passes the commitment (zero for non-privacy)
- PrivacySlasher checks commitment != bytes32(0) before processing — one line
- No bool storage overhead, no function signature forking
A, B, or C?
❯ A
⏺ Question 9 of N: Challenge window — same 100 blocks as policy challenges, or different?
The existing ChallengeVerifier uses taskChallengeWindowBlock (configurable, time-normalized to ~360s across chains). Privacy slashing
could share the same window or have its own.
A) Same window: Both policy and privacy challenges must be submitted within taskChallengeWindowBlock of respondToTask. One window to
reason about. After it expires, the task is finalized — no privacy or policy challenges.
B) Separate window: Privacy challenges have their own privacyChallengeWindowBlock, potentially longer (DLEQ data needs to be fetched from
EigenDA + SP1 proof generated, which takes longer than a ZK policy re-evaluation). But adds configuration complexity.
My recommendation: Option A (same window). The SP1 proof generation for DLEQ (~10-30s) is comparable to or faster than the SP1 Rego
proof. The challenger runs off-chain DLEQ verification immediately (microseconds), only generates the SP1 proof when a violation is
found. The 360s window is more than enough for: detect violation (~1s) + generate SP1 proof (~30s) + submit transaction (~12s). Sharing
the window simplifies the mental model — after the window closes, the task is fully finalized.
A or B?
❯ A
⏺ I believe we've covered all the major design branches. Let me summarize.
DleqVerifier / Privacy Slashing Design Summary (9 decisions)
┌─────┬────────────────┬────────────────────────────────────────────────┬────────────────────────────────────────────────────────────┐
│ # │ Decision │ Choice │ Reasoning │
├─────┼────────────────┼────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ 1 │ Verification │ SP1 ZK circuit │ Least execution risk — no DKG/PSS changes, no new operator │
│ │ approach │ │ dependencies, bug in circuit doesn't corrupt key material │
├─────┼────────────────┼────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤
│ │ │ │ Verifies all operator DLEQ proofs for a task, outputs │
│ 2 │ Circuit scope │ Batch verification │ invalid bitmap. Matches ChallengeLib.slashSigningOperators │
│ │ │ │ pattern │
│ │ │ │ ChallengeLib.slashSigningOperators pattern │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ │ Public: commitmentHash + epochId + taskId. │ │
│ 3 │ Circuit I/O │ Output: invalidOperatorBitmap + re-computed │ Commitment binding prevents fabricated inputs │
│ │ │ commitmentHash for binding │ │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ Data │ │ Gateway posts raw DLEQ data alongside on-chain │
│ 4 │ availability │ EigenDA │ commitment hash. Trustless retrieval, natural for │
│ │ │ │ EigenLayer AVS │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ 5 │ Circuit │ New circuits/sp1-dleq/ │ Separate from sp1-rego — different proof types, clean │
│ │ organization │ │ separation │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ │ Detection │ │ Challenger runs off-chain DLEQ verify (~microseconds) on │
│ 6 │ model │ Verify all (optimistic) │ every privacy task, only generates SP1 proof when │
│ │ │ │ violation found │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ 7 │ Commitment │ Embed in respondToTask │ New fields in TaskResponseMetadata — atomic, one │
│ │ timing │ │ transaction, no commitment gap │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ 8 │ Non-privacy │ Zero-value sentinel │ decryptionCommitment = bytes32(0), PrivacySlasher │
│ │ tasks │ │ rejects challenges on zero commitment │
├─────┼────────────────┼──────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
│ 9 │ Challenge │ Same as policy (taskChallengeWindowBlock) │ ~360s is sufficient for detect + SP1 prove + submit. One │
│ │ window │ │ window simplifies finalization │
└─────┴────────────────┴──────────────────────────────────────────────────┴──────────────────────────────────────────────────────────┘
Implementation order refined:
- NEWT-641: sp1-dleq circuit (replaces Solidity DleqVerifier — SP1 verifier already deployed)
- NEWT-630: On-chain commitments in TaskResponseMetadata + EigenDA blob posting
- NEWT-640: PrivacySlasher contract — verifies SP1 proof, slashes via invalidOperatorBitmap
- Challenger extension: Fetch from EigenDA, off-chain DLEQ verify, SP1 proof on violation
Want me to save this as a plan and update the Linear tickets with the SP1 approach?
Let's proceed to next steps
⏺ The natural next step is NEWT-641 (DleqVerifier library) — it's the first in the dependency chain, has no blockers, and is pure
cryptographic library code.
But before diving into implementation, there's a significant design challenge worth discussing: the curve mismatch. Our DLEQ proofs are
generated on Ristretto255 (Rust side), but EVM precompiles (EIP-196) operate on alt_bn128 (BN254). These are fundamentally different
curves — you can't directly verify a Ristretto255 DLEQ proof using BN254 precompiles.
This needs a /grill-me before implementation. The ticket mentions two approaches:
There may be a third option worth exploring: verifying the DLEQ proof in a ZK circuit (SP1) instead of via EVM precompiles — we already
have the SP1 infrastructure for policy challenges.
Want to start with /grill-me on the DleqVerifier curve strategy, or would you prefer to tackle something else first?
✻ Crunched for 31s
❯ yes /grill-me on the DleqVerifier curve strategy
⏺ Skill(grill-me)
⎿ Successfully loaded skill
⏺ Let me first understand the exact DLEQ proof structure we're working with on the Rust side.
Read 1 file (ctrl+o to expand)
⏺ Good — now I have the exact DLEQ structure. The proof operates on Ed25519 (Curve25519 in Edwards form), not Ristretto255 as I initially
said. The proof is:
This is standard Schnorr-style DLEQ on Ed25519 (group order ~2^252). EVM precompiles operate on BN254 (alt_bn128, group order ~2^254).
These are different curves with different group orders, base points, and coordinate systems.
Question 1 of N: Which verification approach should we use?
Three options with fundamentally different trade-offs:
A) Native Solidity verification on Ed25519
Implement Ed25519 point arithmetic in pure Solidity — scalar multiplication via double-and-add, point addition, SHA-256 hash. No
precompiles needed (SHA-256 is available as a precompile at address 0x02). But Ed25519 scalar mul in Solidity is extremely expensive —
~1M+ gas per verification (no native precompile for Curve25519).
B) SP1 ZK circuit verification
The DLEQ verification runs inside an SP1 zkVM circuit (we already have SP1 infrastructure for sp1-rego policy challenges). The circuit
verifies the DLEQ proof off-chain and produces a succinct proof. On-chain, only the SP1 proof is verified (~300k gas via the SP1 verifier
contract). The operator's DLEQ proof correctness is reduced to "verify this ZK proof."
C) Dual-curve scheme
During DKG/PSS, operators commit their public key share on both Ed25519 (for HPKE threshold decryption) and BN254 (for on-chain DLEQ
verification). Operators generate a second DLEQ proof on BN254 alongside the Ed25519 one. The BN254 proof can be verified on-chain using
EIP-196/197 precompiles (~40k gas). But this doubles the DKG key material and requires operators to maintain two key shares.
My recommendation: Option B (SP1 ZK circuit). Reasons:
A, B, or C?
✻ Churned for 51s
❯ How complex is C? "But this doubles the DKG key material and requires operators to maintain two key shares." --> what does this entail
in terms of complexity, error-prone-ness, reliability, efficiency?
⏺ Good question. Let me break down exactly what dual-curve entails:
What "Two Key Shares" Means Concretely
Currently each operator has:
With dual-curve, each operator would have:
The secret scalar is the same — you don't need two separate secrets. You just compute the public share on both curves. The scalar s_i is
~252 bits; both Ed25519 (order ~2^252) and BN254 (order ~2^254) can accommodate it.
Complexity Assessment
DKG ceremony changes:
~50 lines of code change in the DKG crate. Not trivial but not massive.
PSS refresh changes:
coefficients are scalars)
commitments on both curves, verification works on both independently
Operator runtime changes:
On-chain changes:
Error-Proneness
The risky part: the scalar s_i must be valid in both group orders. Ed25519 order is 2^252 + 27742... and BN254 order is 21888...
(~2^254). Since the BN254 order is larger, any valid Ed25519 scalar is also valid for BN254. This works safely — no modular reduction
needed.
But: if we ever change the DKG to generate scalars mod BN254's order (larger), they might not be valid Ed25519 scalars. This is a latent
footgun. The rule would be: always generate scalars mod Ed25519 order (the smaller one).
FROST interaction: FROST DKG on Ristretto255 generates scalars mod the Ristretto255 group order (same as Ed25519). These are guaranteed
valid for BN254. No issue with the current FROST implementation.
Reliability
Efficiency
┌───────────────────────┬──────────────────────────────┬────────────────────────────────────────────────────┐
│ Aspect │ Current (Ed25519 only) │ Dual-Curve │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ DKG ceremony │ 3 rounds │ 3 rounds (same, +1 point computation per operator) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ Keystore size │ ~200 bytes │ ~264 bytes (+64 bytes for BN254 point) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ PSS refresh │ 1 set of Feldman commitments │ 2 sets (but can share polynomial) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ Prepare phase │ 1 DLEQ proof (Ed25519) │ 1 DLEQ proof (BN254) — can drop Ed25519 proof │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ On-chain verification │ N/A │ ~12k gas (ecMul precompile) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ On-chain storage │ 0 │ ~21k gas per operator per epoch │
└───────────────────────┴──────────────────────────────┴────────────────────────────────────────────────────┘
Compared to SP1 (Option B)
┌───────────────────────────┬───────────────────────────┬───────────────────────────────────┐
│ Aspect │ SP1 ZK Circuit │ Dual-Curve │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ On-chain verification gas │ ~300k (SP1 verifier) │ ~12k (ecMul precompile) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Proof generation time │ ~10-30s (SP1 prover) │ ~0ms (BN254 point mul is instant) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ New dependencies │ None (SP1 already exists) │ ark-bn254 or similar in operator │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ DKG changes │ None │ ~80 lines │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Slashing latency │ Slow (proof generation) │ Instant (precompile verification) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Complexity locus │ Circuit code │ DKG + keystore + refresh │
└───────────────────────────┴───────────────────────────┴───────────────────────────────────┘
My Updated Assessment
Dual-curve is more complex upfront (~80 lines across DKG/PSS/keystore) but dramatically cheaper and faster at verification time (12k gas
vs 300k, instant vs 10-30s proof generation). The complexity is contained in well-tested crypto code (scalar multiplication is
deterministic), not in a new ZK circuit.
SP1 is simpler to implement (no DKG changes) but slower and more expensive when actually slashing.
Revised recommendation: If slashing speed matters (a bad operator should be slashable immediately, not after waiting for SP1 proof
generation), dual-curve is better. If implementation speed matters and slashing is rare, SP1 is better.
What's your priority — fast implementation or fast slashing?
✻ Cooked for 45s
❯ reliability and least error-prone (technically and operationally). Least execution risk.
⏺ With that criteria, Option B (SP1 ZK circuit) is the clear winner.
Here's why:
Dual-curve (C) execution risks:
could produce invalid key material across all operators, requiring a fresh DKG ceremony to recover
shares across epochs
SP1 (B) execution risks:
The decisive factor: with dual-curve, a bug corrupts live key material across all operators. With SP1, a bug means a slashing proof fails
to verify — fixable without touching the privacy system.
Let me continue the grill.
Question 2 of N: SP1 circuit scope — verify a single DLEQ proof, or verify the full commitment?
Two circuit designs:
A) Single DLEQ verification: The circuit takes one operator's DLEQ proof + public inputs (G, pk_i, H, D_i) and outputs pass/fail. The
challenger generates one SP1 proof per operator they want to slash.
B) Commitment batch verification: The circuit takes the full commitment from NEWT-630 (all operator partials + DLEQ proofs for a task)
and verifies all of them, outputting a bitmap of which operators' proofs are invalid. One SP1 proof covers the entire task.
My recommendation: Option A (single DLEQ). Simpler circuit, faster proof generation (one operator's proof vs all), and the PrivacySlasher
only needs to slash one operator at a time. Batch can be added later if needed.
A or B?
✻ Brewed for 30s
❯ B) Commitment batch verification: The circuit takes the full commitment from NEWT-630 (all operator partials + DLEQ proofs for a task)
and verifies all of them, outputting a bitmap of which operators' proofs are invalid. One SP1 proof covers the entire task. -->
similar to how we slashing in ChallengeVerifier
⏺ Exactly — matches the existing pattern in ChallengeLib.slashSigningOperators() which slashes all bad signers in one batch. Consistent
architecture.
Question 3 of N: What are the public inputs and outputs of the SP1 circuit?
The circuit needs to:
Public inputs (committed on-chain, verifiable by anyone):
Private inputs (provided by the challenger, not on-chain):
Public outputs (written to the proof, verified on-chain):
The on-chain PrivacySlasher then:
My recommendation: This structure directly. The commitmentHash binding prevents the challenger from fabricating inputs — they must use
the same data the gateway committed to.
Does this input/output structure look right, or do you want to adjust?
✻ Cogitated for 36s
❯ looks right
⏺ Question 4 of N: Where does the challenger get the raw DLEQ proofs (private inputs)?
The on-chain commitment (NEWT-630) stores only a hash. The challenger needs the actual per-operator data to feed into the SP1 circuit.
Three options:
▐▛███▜▌ Claude Code v2.1.108
▝▜█████▛▘ Opus 4.6 (1M context) with max effort · API Usage Billing
▘▘ ▝▝ ~/projects/newton-prover-avs/contracts
⚠ Large .claude/rules/lessons.md will impact performance (48.3k chars > 40.0k) · /memory to edit
⏺ That's a strong argument for keeping it on-chain. When admin transitions to a DAO, on-chain transactions are the only way the DAO can
interact — there's no "call the gateway RPC" option for a multisig or governance contract. The current design is forward-compatible with
that.
Want me to add that context as a reply on Albert's thread? Something like: "admin will be a DAO in the future — on-chain tx is the only
interface available to governance contracts. keeping it on-chain now avoids a migration later."
❯ update our comment
⏺ Bash(gh api graphql -f query='
mutation {…)
⎿ ok
⏺ Posted. The thread now has the full reasoning: Safe multisig access pattern, block time latency, auditability, and DAO
forward-compatibility.
✻ Churned for 1m 10s
❯ also another engineer asks "that also means an ethereum halt (not often anymore) could compromise the platform for the length of a halt
right?"
⏺ That's a valid concern but the impact is limited. Let me think through it:
An Ethereum halt means no new blocks → the chain watcher can't detect the EmergencyRotationTriggered event → the gateway doesn't trigger
an immediate PSS refresh. But:
emergency rotation, not all rotation. Scheduled rotations are purely off-chain (operator RPC calls) and don't depend on Ethereum being
live.
transactions can land. The privacy data is only useful in the context of on-chain task submissions, which are also blocked.
epoch_duration_seconds (24h default). The emergency signal would reduce that to ~seconds, but during a halt it falls back to the timer.
Should I post this reasoning as a reply? The short version: "Ethereum halt delays the emergency signal, but scheduled rotations are
off-chain and unaffected. The halt also blocks the attacker from exploiting on-chain. Blast radius is bounded by epoch_duration_seconds,
not the halt duration."
❯ what are pro vs cons of the two approaches?
⏺ On-chain emergency signal (current approach)
Pros:
Cons:
Off-chain emergency signal (admin RPC endpoint on gateway)
Pros:
Cons:
The key differentiator is the DAO trajectory. If admin stays as an EOA/multisig with gateway access, off-chain is simpler. Once admin
becomes a DAO, on-chain is the only option. Since we're planning the DAO transition, on-chain is the forward-compatible choice.
Want me to post a summary of this on the PR thread?
✻ Cooked for 31s
❯ this PR has been merged. Let's update Linear project task status to reflect the current progress
⏺ Let me check the current status of NEWT-639 and update it:
Called linear-server (ctrl+o to expand)
⏺ NEWT-639 is already marked Done (auto-closed by PR #544 merge). The PR attachment is linked. Let me check the remaining tickets to see if
any need status updates now that NEWT-639 is complete:
Called linear-server (ctrl+o to expand)
⏺ Linear is already up to date. Here's the current state:
Done (auto-closed by PR merges):
Remaining Backlog (Phase 2D Solidity, unchanged):
┌──────────┬────────────────────────────────────┬─────────┐
│ Ticket │ Title │ Status │
├──────────┼────────────────────────────────────┼─────────┤
│ NEWT-629 │ Privacy slashing conditions (spec) │ Backlog │
├──────────┼────────────────────────────────────┼─────────┤
│ NEWT-640 │ PrivacySlasher contract │ Backlog │
├──────────┼────────────────────────────────────┼─────────┤
│ NEWT-641 │ DleqVerifier library │ Backlog │
├──────────┼────────────────────────────────────┼─────────┤
│ NEWT-630 │ On-chain aggregation commitments │ Backlog │
└──────────┴────────────────────────────────────┴─────────┘
Far future (Phase 3/4): MPC/ZK, mainnet deployment, ecosystem — all Backlog.
No manual status updates needed — the GitHub integration auto-marked NEWT-639 as Done when PR #544 merged. The remaining backlog tickets
are correctly in Backlog status, unblocked and ready for when we pick them up.
✻ Cooked for 1m 6s
❯ analyze the importance and implication of phase 2D. What is our current state in terms of newton privacy layer, and what does phase 2D
bring and why important?
⏺ ★ Insight ─────────────────────────────────────
This is a critical architectural question. Phase 2D is the economic enforcement layer — without it, the privacy system relies entirely on
operator honesty. Let me analyze what we have, what's missing, and why it matters.
─────────────────────────────────────────────────
Current State: Privacy Layer without Economic Enforcement
What we have (Phases 1→2C, all merged):
What's missing (Phase 2D):
What Phase 2D Brings
NEWT-629: Slashing Conditions (spec)
Defines three provable violation types:
┌─────────────────────┬──────────────────────────────────────────────┬──────────────────────────────────────────────────┬───────────┐
│ Violation │ Detection │ Proof Type │ Severity │
├─────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────────────┼───────────┤
│ Wrong-keyshare │ Operator submits an invalid partial DH │ DLEQ proof failure (deterministic, │ 10% slash │
│ │ output │ cryptographic) │ │
├─────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────────────┼───────────┤
│ No-keyshare │ Operator absent during threshold decryption │ Quorum attestation (t-of-n operators attest to │ 1-2% │
│ │ window │ absence) │ slash │
├─────────────────────┼──────────────────────────────────────────────┼──────────────────────────────────────────────────┼───────────┤
│ Premature │ Operator participates in unauthorized task │ On-chain authorization check against │ 10% slash │
│ decryption │ │ EpochRegistry │ │
└─────────────────────┴──────────────────────────────────────────────┴──────────────────────────────────────────────────┴───────────┘
NEWT-641: DleqVerifier Library
Solidity library using EIP-196 precompiles (bn128 alt_bn128) to verify DLEQ proofs on-chain:
Prove: log_G(PK_i) == log_{enc}(D_i)
This is the cryptographic primitive that makes "wrong-keyshare" provable on-chain. Without it, you can only detect wrong shares off-chain
(which is what the Rust DLEQ code already does), but you can't slash for it.
NEWT-640: PrivacySlasher Contract
Calls EigenLayer's InstantSlasher.fulfillSlashingRequest() when a violation is proven. References EpochRegistry.isEpochActive() and
getEpoch() to validate that the slashing claim is against a valid epoch.
Why Phase 2D Matters
Without Phase 2D, the privacy system is trust-based, not trust-minimized.
Right now, operators can:
zero economic consequences. They can keep doing it.
participate, the task succeeds, but the attack is free.
The practical impact:
(needs t operators) with no slashing risk.
— without slashing, privacy is outside that model.
How Critical is Phase 2D?
For testnet/staging: Not blocking. The current system works correctly — DLEQ proofs verify off-chain, bad operators are excluded from
aggregation, and the permissioned operator set provides practical security.
For mainnet with real money: Phase 2D is a prerequisite. You can't launch privacy features with restaked ETH security claims unless
misbehavior has economic consequences. The DLEQ proof is the key differentiator from other threshold encryption systems — it makes
wrong-keyshare provable on-chain, not just detectable.
For the DAO transition: The PrivacySlasher is permissionless (anyone can submit a slashing proof). This aligns with decentralization —
you don't need a trusted entity to police operators.
Recommendation
Phase 2D is necessary for production but not urgent for the current testnet milestone. The natural order:
The DLEQ verification is the hard part — it requires mapping the Ristretto255 DLEQ proof to bn128 pairing-friendly operations on-chain,
which involves non-trivial curve arithmetic. That's why NEWT-641 exists as a separate library.
✻ Sautéed for 52s
❯ How does privacy slashing work in had with the current slashing logic we have? For example, if policy evaluation was correct, but
privacy decryption was incorrect, who get's slashed? is it just privacy decyrption operated operators?
⏺ ★ Insight ─────────────────────────────────────
This is a critical trust model question. Newton currently has two independent slashing mechanisms, and Phase 2D adds a third.
Understanding how they compose is essential for the spec.
─────────────────────────────────────────────────
Current Slashing Architecture
Let me check how the existing slashing works to give a precise answer:
Searched for 2 patterns (ctrl+o to expand)
⏺ Now I have the full picture. Here's how the two slashing systems interact:
Current Slashing: Policy Evaluation Correctness
The existing system slashes operators for incorrect policy evaluation results:
operator's evaluationResult was wrong, ChallengeLib.slashSigningOperators() slashes all operators who BLS-signed the incorrect response
at 10% (WADS_TO_SLASH = 100000000000000000).
Who gets slashed: Every operator whose BLS signature is in the aggregated response. The BLS aggregation means all signers are
collectively liable — you can't distinguish which individual operator produced a wrong sub-result.
Phase 2D: Privacy Decryption Correctness
Privacy slashing is fundamentally different from policy slashing because it targets a different failure mode:
┌─────────────────┬───────────────────────────────────────┬─────────────────────────────────────────────────┐
│ Aspect │ Policy Slashing (current) │ Privacy Slashing (Phase 2D) │
├─────────────────┼───────────────────────────────────────┼─────────────────────────────────────────────────┤
│ What failed │ Policy evaluation result was wrong │ Threshold decryption share was wrong or missing │
├─────────────────┼───────────────────────────────────────┼─────────────────────────────────────────────────┤
│ Who is liable │ All BLS signers (collective) │ Individual operator (targeted) │
├─────────────────┼───────────────────────────────────────┼─────────────────────────────────────────────────┤
│ Proof type │ ZK proof of correct evaluation (SP1) │ DLEQ proof of correct partial DH │
├─────────────────┼───────────────────────────────────────┼─────────────────────────────────────────────────┤
│ Detection │ Challenger re-evaluates independently │ Gateway verifies each operator's DLEQ proof │
├─────────────────┼───────────────────────────────────────┼─────────────────────────────────────────────────┤
│ When it happens │ After respondToTask on-chain │ During Prepare phase (off-chain, before BLS) │
└─────────────────┴───────────────────────────────────────┴─────────────────────────────────────────────────┘
The Key Interaction: Your Scenario
"Policy evaluation correct, but privacy decryption incorrect"
This is the exact scenario Phase 2D addresses. Here's the timeline:
proof.
Operator A and proceeds with the remaining t-1 operators (if enough remain).
result is correct.
Without Phase 2D: Operator A submitted a bad share, was excluded, and faces zero consequences. They can keep doing this — degrading
liveness (fewer operators available for threshold) without any economic risk.
With Phase 2D: The gateway submits Operator A's invalid DLEQ proof to the PrivacySlasher contract. The DleqVerifier library verifies the
proof is indeed invalid on-chain. Operator A gets slashed 10%. Only Operator A — not the other honest operators.
Separation of Concerns
The two slashing systems are independent and composable:
Policy slashing: BLS signers of wrong result → 10% slash (collective)
Privacy slashing: Individual bad decryptor → 10% slash (targeted)
An operator could be slashed by both systems in theory:
But in practice, the bad decryption is caught in the Prepare phase (before BLS signing), so the operator is excluded before they can sign
a wrong result. The two failure modes are temporally separated.
What About "No-Keyshare" (Liveness)?
The 1-2% slash for absence is lighter because:
This is similar to how EigenLayer handles general liveness failures, just scoped to the privacy decryption window.
✻ Crunched for 1m 21s
❯ But in this model, Gateway is being trusted that it will honestly submit onchain to slash the bad operators. Isn't it? is there a way
for the current challenger to do so?
⏺ You've identified the critical trust assumption. Yes — in the current Phase 2D design, the gateway is the only entity that sees the raw
DLEQ proofs during the Prepare phase. If the gateway is compromised or colluding with a bad operator, it can simply not submit the
slashing proof.
The Trust Gap
┌───────────────────────────────┬─────────────────────────┬──────────────────┬─────────────────────────────────────────────────────┐
│ Slashing Type │ Who Detects │ Who Submits │ Trust Assumption │
│ │ │ Proof │ │
├───────────────────────────────┼─────────────────────────┼──────────────────┼─────────────────────────────────────────────────────┤
│ Policy (current) │ Challenger │ Challenger │ Trustless — anyone can re-evaluate and challenge │
│ │ (independent) │ │ │
├───────────────────────────────┼─────────────────────────┼──────────────────┼─────────────────────────────────────────────────────┤
│ Privacy (Phase 2D as │ Gateway │ Gateway │ Trusted — gateway must be honest about DLEQ │
│ designed) │ │ │ failures │
└───────────────────────────────┴─────────────────────────┴──────────────────┴─────────────────────────────────────────────────────┘
The current challenger cannot detect privacy decryption failures because:
Could the Challenger Do It?
For the challenger to independently verify privacy decryption correctness, it would need:
Option A: On-chain DLEQ commitments (NEWT-630)
This is actually what NEWT-630 (on-chain aggregation commitments) is designed for. If the gateway publishes a commitment of all partial
decryptions + DLEQ proofs on-chain (or to EigenDA), then:
This makes privacy slashing trustless — same model as policy slashing. The gateway can't hide a bad operator because the commitment is
on-chain.
Option B: Operators report each other
In the Prepare phase, each operator receives partial decryptions from peers (in a broadcast model). If Operator B sees Operator A's
invalid DLEQ proof, Operator B could submit the slashing proof directly. But currently the gateway mediates all communication — operators
don't see each other's proofs.
Option C: Broadcast partial decryptions + proofs publicly
Post all DLEQ proofs to a public data availability layer. Anyone can verify. But this leaks partial decryption data which has privacy
implications.
The Right Architecture
The combination of NEWT-630 (commitments) + NEWT-640 (PrivacySlasher) makes it trustless:
Prepare phase:
Gateway collects partials + DLEQ proofs from operators
Gateway verifies DLEQ proofs (excludes bad operators)
Gateway posts commitment root on-chain (NEWT-630)
Challenge window:
Challenger fetches individual proofs via EigenDA or gateway
Challenger verifies each DLEQ proof independently
If invalid: submits to PrivacySlasher (NEWT-640)
This is why NEWT-630 exists alongside NEWT-640/641 — without the commitments, the slashing is gateway-trusted. With commitments, any
external party can verify and slash, matching the existing challenger model for policy evaluation.
Practical Recommendation
For the implementation order, I'd suggest:
This way the slashing system is trustless from day one, matching Newton's existing challenger model. Want me to update the Linear tickets
to reflect this dependency ordering and the commitment requirement?
✻ Brewed for 31s
❯ yes
Called linear-server (ctrl+o to expand)
⏺ Good — NEWT-630 already has the commitment architecture described in detail, and it already blocks NEWT-629. But the description needs
updating to reflect the DLEQ-based trustless slashing model we just discussed. Let me also check NEWT-629's current relations:
Called linear-server (ctrl+o to expand)
⏺ Good — the dependency graph already has NEWT-630 blocking NEWT-629, and NEWT-628 blocking NEWT-629 (done). Now let me update the tickets
to reflect the trustless slashing model and correct implementation order:
Called linear-server (ctrl+o to expand)
⏺ Now add the blocking relation — NEWT-641 should block NEWT-640 (PrivacySlasher needs DleqVerifier):
Called linear-server (ctrl+o to expand)
⏺ Now add NEWT-630 blocking NEWT-640 (PrivacySlasher needs commitments for trustless detection):
Called linear-server (ctrl+o to expand)
⏺ Now update NEWT-640 (PrivacySlasher) to reflect that it depends on both NEWT-641 and NEWT-630:
Called linear-server (ctrl+o to expand)
⏺ All 4 tickets updated. Here's the final dependency graph:
NEWT-628 (PSS epoch rotation) ──── DONE
└── NEWT-639 (EpochRegistry) ──── DONE
NEWT-641 (DleqVerifier library) ──── Backlog
└── blocks NEWT-640
NEWT-630 (On-chain commitments) ──── Backlog
├── blocks NEWT-629
└── blocks NEWT-640
NEWT-629 (Slashing spec) ──── Backlog, blocked by NEWT-628 (done) + NEWT-630
├── NEWT-640 (PrivacySlasher) ──── Backlog, blocked by NEWT-641 + NEWT-630
└── NEWT-641 (DleqVerifier) ──── Backlog
Implementation order:
Key updates made:
slashing comparison table
✻ Sautéed for 2m 9s
❯ one more question. is PrivacySlasher purely for Prepare phase?
⏺ No — it covers both phases, but different violation types map to different phases:
┌──────────────────────────┬───────────┬─────────────────────────────────────────────────────────────────────────────────────────────┐
│ Violation │ Phase │ Why │
├──────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Wrong-keyshare (DLEQ │ Prepare │ Operators submit partial DH outputs during Prepare. The DLEQ proof accompanies each │
│ failure) │ phase │ partial. │
├──────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ No-keyshare (liveness) │ Prepare │ Operator fails to respond during the Prepare phase timeout. │
│ │ phase │ │
├──────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Wrong BLS signature on │ Commit │ Operator BLS-signs a policy result that was computed from incorrect decrypted data. But │
│ privacy result │ phase │ this is already covered by the existing policy challenger (ZK proof) — not PrivacySlasher's │
│ │ │ scope. │
└──────────────────────────┴───────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
So practically, the PrivacySlasher targets Prepare phase misbehavior — that's where threshold decryption happens. The Commit phase
(policy evaluation + BLS signing) is already covered by the existing ChallengeVerifier.
There's one edge case worth noting: in the gateway-never-decrypts model, operators also decrypt locally during the Commit phase (they
receive partial DH outputs from Prepare and complete HPKE decryption). But that local decryption is deterministic given correct partials
— if the partials were valid (verified via DLEQ in Prepare), the Commit-phase decryption can't be wrong. So there's no separate
Commit-phase privacy violation to slash for.
The flow:
Prepare phase: partials + DLEQ proofs → PrivacySlasher can slash
Commit phase: policy eval + BLS sign → ChallengeVerifier can slash (existing)
Two independent slashing systems, one per phase. No overlap, no gaps.
✻ Cogitated for 30s
❯ Let's proceed to next steps
⏺ The natural next step is NEWT-641 (DleqVerifier library) — it's the first in the dependency chain, has no blockers, and is pure
cryptographic library code.
But before diving into implementation, there's a significant design challenge worth discussing: the curve mismatch. Our DLEQ proofs are
generated on Ristretto255 (Rust side), but EVM precompiles (EIP-196) operate on alt_bn128 (BN254). These are fundamentally different
curves — you can't directly verify a Ristretto255 DLEQ proof using BN254 precompiles.
This needs a /grill-me before implementation. The ticket mentions two approaches:
There may be a third option worth exploring: verifying the DLEQ proof in a ZK circuit (SP1) instead of via EVM precompiles — we already
have the SP1 infrastructure for policy challenges.
Want to start with /grill-me on the DleqVerifier curve strategy, or would you prefer to tackle something else first?
✻ Crunched for 31s
❯ yes /grill-me on the DleqVerifier curve strategy
⏺ Skill(grill-me)
⎿ Successfully loaded skill
⏺ Let me first understand the exact DLEQ proof structure we're working with on the Rust side.
Read 1 file (ctrl+o to expand)
⏺ Good — now I have the exact DLEQ structure. The proof operates on Ed25519 (Curve25519 in Edwards form), not Ristretto255 as I initially
said. The proof is:
This is standard Schnorr-style DLEQ on Ed25519 (group order ~2^252). EVM precompiles operate on BN254 (alt_bn128, group order ~2^254).
These are different curves with different group orders, base points, and coordinate systems.
Question 1 of N: Which verification approach should we use?
Three options with fundamentally different trade-offs:
A) Native Solidity verification on Ed25519
Implement Ed25519 point arithmetic in pure Solidity — scalar multiplication via double-and-add, point addition, SHA-256 hash. No
precompiles needed (SHA-256 is available as a precompile at address 0x02). But Ed25519 scalar mul in Solidity is extremely expensive —
~1M+ gas per verification (no native precompile for Curve25519).
B) SP1 ZK circuit verification
The DLEQ verification runs inside an SP1 zkVM circuit (we already have SP1 infrastructure for sp1-rego policy challenges). The circuit
verifies the DLEQ proof off-chain and produces a succinct proof. On-chain, only the SP1 proof is verified (~300k gas via the SP1 verifier
contract). The operator's DLEQ proof correctness is reduced to "verify this ZK proof."
C) Dual-curve scheme
During DKG/PSS, operators commit their public key share on both Ed25519 (for HPKE threshold decryption) and BN254 (for on-chain DLEQ
verification). Operators generate a second DLEQ proof on BN254 alongside the Ed25519 one. The BN254 proof can be verified on-chain using
EIP-196/197 precompiles (~40k gas). But this doubles the DKG key material and requires operators to maintain two key shares.
My recommendation: Option B (SP1 ZK circuit). Reasons:
A, B, or C?
✻ Churned for 51s
❯ How complex is C? "But this doubles the DKG key material and requires operators to maintain two key shares." --> what does this entail
in terms of complexity, error-prone-ness, reliability, efficiency?
⏺ Good question. Let me break down exactly what dual-curve entails:
What "Two Key Shares" Means Concretely
Currently each operator has:
With dual-curve, each operator would have:
The secret scalar is the same — you don't need two separate secrets. You just compute the public share on both curves. The scalar s_i is
~252 bits; both Ed25519 (order ~2^252) and BN254 (order ~2^254) can accommodate it.
Complexity Assessment
DKG ceremony changes:
~50 lines of code change in the DKG crate. Not trivial but not massive.
PSS refresh changes:
coefficients are scalars)
commitments on both curves, verification works on both independently
Operator runtime changes:
On-chain changes:
Error-Proneness
The risky part: the scalar s_i must be valid in both group orders. Ed25519 order is 2^252 + 27742... and BN254 order is 21888...
(~2^254). Since the BN254 order is larger, any valid Ed25519 scalar is also valid for BN254. This works safely — no modular reduction
needed.
But: if we ever change the DKG to generate scalars mod BN254's order (larger), they might not be valid Ed25519 scalars. This is a latent
footgun. The rule would be: always generate scalars mod Ed25519 order (the smaller one).
FROST interaction: FROST DKG on Ristretto255 generates scalars mod the Ristretto255 group order (same as Ed25519). These are guaranteed
valid for BN254. No issue with the current FROST implementation.
Reliability
Efficiency
┌───────────────────────┬──────────────────────────────┬────────────────────────────────────────────────────┐
│ Aspect │ Current (Ed25519 only) │ Dual-Curve │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ DKG ceremony │ 3 rounds │ 3 rounds (same, +1 point computation per operator) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ Keystore size │ ~200 bytes │ ~264 bytes (+64 bytes for BN254 point) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ PSS refresh │ 1 set of Feldman commitments │ 2 sets (but can share polynomial) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ Prepare phase │ 1 DLEQ proof (Ed25519) │ 1 DLEQ proof (BN254) — can drop Ed25519 proof │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ On-chain verification │ N/A │ ~12k gas (ecMul precompile) │
├───────────────────────┼──────────────────────────────┼────────────────────────────────────────────────────┤
│ On-chain storage │ 0 │ ~21k gas per operator per epoch │
└───────────────────────┴──────────────────────────────┴────────────────────────────────────────────────────┘
Compared to SP1 (Option B)
┌───────────────────────────┬───────────────────────────┬───────────────────────────────────┐
│ Aspect │ SP1 ZK Circuit │ Dual-Curve │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ On-chain verification gas │ ~300k (SP1 verifier) │ ~12k (ecMul precompile) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Proof generation time │ ~10-30s (SP1 prover) │ ~0ms (BN254 point mul is instant) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ New dependencies │ None (SP1 already exists) │ ark-bn254 or similar in operator │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ DKG changes │ None │ ~80 lines │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Slashing latency │ Slow (proof generation) │ Instant (precompile verification) │
├───────────────────────────┼───────────────────────────┼───────────────────────────────────┤
│ Complexity locus │ Circuit code │ DKG + keystore + refresh │
└───────────────────────────┴───────────────────────────┴───────────────────────────────────┘
My Updated Assessment
Dual-curve is more complex upfront (~80 lines across DKG/PSS/keystore) but dramatically cheaper and faster at verification time (12k gas
vs 300k, instant vs 10-30s proof generation). The complexity is contained in well-tested crypto code (scalar multiplication is
deterministic), not in a new ZK circuit.
SP1 is simpler to implement (no DKG changes) but slower and more expensive when actually slashing.
Revised recommendation: If slashing speed matters (a bad operator should be slashable immediately, not after waiting for SP1 proof
generation), dual-curve is better. If implementation speed matters and slashing is rare, SP1 is better.
What's your priority — fast implementation or fast slashing?
✻ Cooked for 45s
❯ reliability and least error-prone (technically and operationally). Least execution risk.
⏺ With that criteria, Option B (SP1 ZK circuit) is the clear winner.
Here's why:
Dual-curve (C) execution risks:
could produce invalid key material across all operators, requiring a fresh DKG ceremony to recover
shares across epochs
SP1 (B) execution risks:
The decisive factor: with dual-curve, a bug corrupts live key material across all operators. With SP1, a bug means a slashing proof fails
to verify — fixable without touching the privacy system.
Let me continue the grill.
Question 2 of N: SP1 circuit scope — verify a single DLEQ proof, or verify the full commitment?
Two circuit designs:
A) Single DLEQ verification: The circuit takes one operator's DLEQ proof + public inputs (G, pk_i, H, D_i) and outputs pass/fail. The
challenger generates one SP1 proof per operator they want to slash.
B) Commitment batch verification: The circuit takes the full commitment from NEWT-630 (all operator partials + DLEQ proofs for a task)
and verifies all of them, outputting a bitmap of which operators' proofs are invalid. One SP1 proof covers the entire task.
My recommendation: Option A (single DLEQ). Simpler circuit, faster proof generation (one operator's proof vs all), and the PrivacySlasher
only needs to slash one operator at a time. Batch can be added later if needed.
A or B?
✻ Brewed for 30s
❯ B) Commitment batch verification: The circuit takes the full commitment from NEWT-630 (all operator partials + DLEQ proofs for a task)
and verifies all of them, outputting a bitmap of which operators' proofs are invalid. One SP1 proof covers the entire task. -->
similar to how we slashing in ChallengeVerifier
⏺ Exactly — matches the existing pattern in ChallengeLib.slashSigningOperators() which slashes all bad signers in one batch. Consistent
architecture.
Question 3 of N: What are the public inputs and outputs of the SP1 circuit?
The circuit needs to:
Public inputs (committed on-chain, verifiable by anyone):
Private inputs (provided by the challenger, not on-chain):
Public outputs (written to the proof, verified on-chain):
The on-chain PrivacySlasher then:
My recommendation: This structure directly. The commitmentHash binding prevents the challenger from fabricating inputs — they must use
the same data the gateway committed to.
Does this input/output structure look right, or do you want to adjust?
✻ Cogitated for 36s
❯ looks right
⏺ Question 4 of N: Where does the challenger get the raw DLEQ proofs (private inputs)?
The on-chain commitment (NEWT-630) stores only a hash. The challenger needs the actual per-operator data to feed into the SP1 circuit.
Three options:
A) EigenDA: Gateway posts the raw DLEQ data to EigenDA alongside the on-chain commitment hash. EigenDA provides data availability
guarantees — the data is retrievable by anyone for the dispute window. Cost: ~$0.01 per blob. This is how EigenLayer-native systems
typically handle DA.
B) Gateway API endpoint: Gateway exposes a newt_getDecryptionProofs(taskId) RPC endpoint that returns the raw data. Simpler but relies on
the gateway being online and honest about serving the data. A compromised gateway could refuse to serve proofs for tasks where it
colluded.
C) Operator-direct: Challenger contacts each operator directly (via their socket address) to re-request their partial decryption + DLEQ
proof. Operators are incentivized to respond (if they're honest, they want the bad operator slashed). No single point of failure.
My recommendation: Option A (EigenDA). Reasons:
A, B, or C?