-
-
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)? |
Project: Newton Privacy Layer
Date: 2026-04-14
Summary: Completed PSS epoch rotation (NEWT-628) — all 3 stacked PRs (#523, #525, #526) merged to main after addressing reviewer feedback
from 0xkr8os. Addressed 12 review comments across the stack (7 on #523, 5 on #525, 4 on #526), fixing: module extraction, field name
consistency, feature gating, RPC helper dedup, BFT threshold formula, and slither CI config. Also merged gateway-never-decrypts (#533)
and multi-domain resolution (#534). Privacy Layer Phase 2C Rust code is now complete.
Implementation Progress:
- PR #523 MERGED: PSS core primitives, types, keystore, metrics
- PR #525 MERGED: RefreshCoordinator state machine, operator handlers, EpochManager
- PR #526 MERGED: E2E test, coordinator HTTP wiring, zero TODOs
- PR #522 MERGED: make dkg command
- PR #533 MERGED: gateway-never-decrypts
- PR #534 MERGED: multi-domain resolution + threshold for all privacy types
Key Review Feedback Applied:
- Extracted refresh_types + handlers from core.rs (2235 LOC) to crates/operator/src/refresh.rs
- Renamed RefreshRoundResponse.shares → encrypted_shares for consistency with gateway
- Gated refresh, refresh_coordinator, epoch_manager modules behind frost-dkg feature
- Consolidated call_operator_rpc and sign_dkg_message (removed duplicates in refresh_coordinator)
- Changed run_refresh to take &Arc<Mutex> instead of owned Arc
- Fixed epoch threshold: max(2, ceil(2n/3)) instead of unanimous
- Fixed slither CI: comma-separated strings instead of JSON arrays
Learnings:
- self: &Arc<Mutex> method receiver requires nightly arbitrary_self_types — use &Arc as regular param instead
- Feature gating must cascade: if module A imports gated module B, A must also be gated (or use cfg-conditional imports)
- Slither 0.10.4+ changed config from JSON arrays to comma-separated strings — unpinned CI tools break silently
Remaining Work (Phase 2 Solidity):
- NEWT-639: EpochRegistry contract (backlog, unblocked)
- NEWT-629: Privacy slashing conditions (backlog, unblocked by NEWT-628)
- NEWT-640: PrivacySlasher contract (backlog)
- NEWT-641: DleqVerifier library (backlog)
- NEWT-630: On-chain aggregation commitments (backlog)
Current State: All Privacy Layer Rust code (Phases 1→2C) is complete and merged. Remaining work is exclusively Solidity contracts for
on-chain epoch registry and privacy-specific slashing.
Good context gathered. Now let me start the grill.
Question 1 of N: Proxy vs Non-Proxy Deployment
The existing registries (IdentityRegistry, ConfidentialDataRegistry, PolicyClientRegistry) all use TransparentUpgradeableProxy with
OwnableUpgradeable. However, BatchTaskManager is deployed without a proxy (lessons.md: "not proxied — redeploy is the only upgrade
path").
For EpochRegistry, the decision matters because:
- Proxied: upgradeable, keeps address stable across versions, but adds gas overhead and storage layout constraints
- Non-proxied: simpler, cheaper gas, but requires redeployment + re-authorization to upgrade
The EpochRegistry stores epoch history (MPK, operator set hashes, destruction attestations). If we redeploy, we lose that history unless
we migrate it.
My recommendation: Proxy (TransparentUpgradeableProxy). The epoch history is valuable for auditability and slashing disputes — losing it
on upgrade is unacceptable. The PrivacySlasher (NEWT-640) will need to reference historical epochs to verify slashing claims. Same
pattern as the other registries.
Do you agree with proxied deployment, or should this be non-proxied like BatchTaskManager?
✻ Brewed for 2m 33s
❯ agree
⏺ Question 2 of N: Who can call registerEpoch?
The spec says "Gateway/aggregator only (task generator role)." In practice, the EpochManager in the gateway drives PSS refresh and would
call registerEpoch after a successful ceremony. Let me check who calls what in the existing pattern:
- IdentityRegistry.registerIdentityData — requires gateway EIP-712 co-signature, verified via
operatorRegistry.isTaskGenerator(recoveredSigner) - ConfidentialDataRegistry.publishData — permissionless (any registered provider)
- NewtonProverTaskManager.createTask — checks isTaskGenerator(msg.sender)
For EpochRegistry, the caller is the gateway's EpochManager, which signs transactions with task_generator_signer (same ECDSA key used for
task creation and DKG ceremonies).
Two options:
- A) require(operatorRegistry.isTaskGenerator(msg.sender)) — simple, direct msg.sender check. Same pattern as createTask. Gateway calls
directly. - B) EIP-712 signature verification — gateway signs off-chain, anyone can relay. More complex but enables gasless submission by a
relayer.
My recommendation: Option A — direct isTaskGenerator(msg.sender). Epoch registration is a low-frequency operation (once per epoch,
default 24h). There's no need for relayer patterns. The gateway already has the signer key and submits on-chain transactions for task
responses. Keeps the contract simpler and cheaper.
A or B?
❯ A
⏺ Question 3 of N: Should EpochRegistry take operatorRegistry as a constructor immutable or as an initializer parameter?
Looking at the existing pattern:
- IdentityRegistry takes operatorRegistry and policyClientRegistry as immutable constructor parameters (set at implementation deploy
time, not in initialize()) - ConfidentialDataRegistry takes policyClientRegistry as an immutable constructor parameter
- These are immutable because the registry addresses don't change — if they did, you'd redeploy the implementation and upgrade the proxy
For EpochRegistry, we need operatorRegistry to call isTaskGenerator(msg.sender). The operator registry address is stable per chain
(deployed once, upgraded in place).
My recommendation: Immutable constructor parameter, matching IdentityRegistry/ConfidentialDataRegistry pattern:
contract EpochRegistry is Initializable, OwnableUpgradeable {
IOperatorRegistry public immutable operatorRegistry;
constructor(IOperatorRegistry _operatorRegistry) {
operatorRegistry = _operatorRegistry;
_disableInitializers();
}
function initialize(address owner) external initializer {
__Ownable_init(owner);
}
}
Agree?
❯ yes
⏺ Question 4 of N: What fields belong in EpochInfo and what's the on-chain storage model?
The spec proposes:
struct EpochInfo {
uint64 epochId;
bytes32 mpk; // Master Public Key (X25519, 32 bytes)
bytes32 operatorSetHash; // keccak256 of participating operator addresses
uint8 threshold; // t in t-of-n
uint8 committeeSize; // n
uint256 startBlock;
uint256 endBlock; // 0 if current epoch
uint256 dkgCeremonyBlock;
}
A few design questions nested here:
MPK format: The Rust side uses Edwards Y (compressed, 32 bytes) which converts to X25519 for HPKE. Storing bytes32 is fine — consumers
interpret it. But should we store the Edwards point or the X25519 point? The PrivacySlasher (NEWT-640) will need to verify DLEQ proofs
against operator public shares, which use Edwards. Clients encrypt to X25519.
My recommendation: Store Edwards Y (bytes32). The X25519 conversion is deterministic and can be done off-chain. DLEQ verification
(NEWT-641) needs the Edwards form. Add a comment clarifying the encoding.
endBlock: The spec uses 0 for "current epoch" and sets it when a new epoch registers. But this means registerEpoch for epoch N+1 must
also write to epoch N's storage slot (to set endBlock). That's an extra SSTORE (~5k gas for warm slot).
My recommendation: Drop endBlock. The end of epoch N is implicitly startBlock of epoch N+1. One less storage write, and isEpochActive can
compute it from the next epoch's startBlock. Store a currentEpochId state variable instead — getEpoch(currentEpochId) gives the active
epoch.
dkgCeremonyBlock: When is this useful? The DKG/PSS ceremony happens off-chain. This block number would be the block at which
registerEpoch was called, which is just startBlock. Seems redundant.
My recommendation: Drop dkgCeremonyBlock. If we need ceremony provenance, the transaction hash of registerEpoch is sufficient.
Proposed trimmed struct:
struct EpochInfo {
uint64 epochId;
bytes32 mpk; // Compressed Edwards Y of threshold master public key
bytes32 operatorSetHash; // keccak256(abi.encodePacked(sorted operator addresses))
uint8 threshold; // t in t-of-n
uint8 committeeSize; // n
uint256 startBlock; // block.number at registration
}
Plus state: uint64 public currentEpochId;
Agree with trimming endBlock and dkgCeremonyBlock?
❯ agree
⏺ Question 5 of N: Grace period — block-based or time-based?
The spec says "configurable duration (default: 600 blocks ~2h)" and the Rust side uses grace_period_epochs: u32 (number of past epochs to
keep, default 2).
These are two different models:
- A) Block-based grace: epoch N enters grace period when epoch N+1 registers, expires after gracePeriodBlocks blocks. Requires
isEpochActive to check block.number < epochs[epochId].startBlock + gracePeriodBlocks. But block times vary per chain (12s L1, 2s L2) —
600 blocks is ~2h on L1 but ~20min on L2. - B) Epoch-count grace: epoch N is active if currentEpochId - epochId <= gracePeriodEpochs. Simple integer comparison. Chain-agnostic.
Matches the Rust side's grace_period_epochs config. - C) Timestamp-based grace: use block.timestamp instead of block.number. Chain-agnostic but block.timestamp is manipulable by validators
(within bounds).
My recommendation: Option B — epoch-count grace. Reasons:
- Matches the Rust side's existing model (grace_period_epochs: 2)
- Chain-agnostic — no time-normalization needed per chain
- Simplest check: currentEpochId - epochId <= gracePeriodEpochs
- The PrivacySlasher needs to know "is this epoch still valid for slashing?" — epoch-count is the cleanest predicate
- Store gracePeriodEpochs as a configurable uint8 (owner-settable)
isEpochActive(epochId) becomes:
function isEpochActive(uint64 epochId) external view returns (bool) {
if (epochId > currentEpochId || epochs[epochId].startBlock == 0) return false;
return currentEpochId - epochId <= gracePeriodEpochs;
}
A, B, or C?
❯ B
⏺ Question 6 of N: Destruction attestations — do we need them on-chain?
The spec includes recordDestructionAttestation(epochId, operator, signature) — operators attest that they've destroyed their old epoch
key shares. This is a forward-secrecy signal: "I no longer hold shares for epoch N."
But thinking through the threat model:
- Honest operator: destroys shares after grace period, may or may not bother submitting an on-chain attestation (costs gas)
- Malicious operator: retains shares regardless — an on-chain attestation proving destruction is unforgeable in theory, but an operator
can sign the attestation and still keep the shares. There's no cryptographic proof of deletion. - Verification: nobody can verify that shares were actually destroyed. The attestation is a trust signal, not a proof.
Each attestation is one SSTORE per operator per epoch. With 20 operators and epoch-count grace of 2, that's 20 transactions every ~24h
just for attestations that can't be verified.
Three options:
- A) Keep on-chain attestations: trust signal for compliance/audit, even if unverifiable. PrivacySlasher could require attestation before
releasing operator from epoch liability. - B) Off-chain attestations only: operators sign attestations, gateway stores them. Available via RPC for auditors. Zero gas cost.
- C) Drop entirely: old shares are overwritten by PSS refresh (the accumulate_refresh replaces the share in memory + keystore). The
protocol already enforces rotation — explicit destruction attestation adds ceremony without security.
My recommendation: Option C — drop destruction attestations. PSS refresh inherently replaces shares. The keystore delete_epoch_keystore
already removes old files after grace period. Adding unverifiable on-chain attestations is compliance theater that costs gas without
improving security. If auditors need proof of rotation, the EpochRegistered events form a chain of epoch transitions — that's the
verifiable signal.
A, B, or C?
❯ C
⏺ Question 7 of N: Emergency rotation — what should it actually do?
The spec includes triggerEmergencyRotation() (admin only). The question is what this does on-chain vs off-chain:
The actual PSS refresh ceremony is entirely off-chain (gateway coordinates operators via RPC). The on-chain EpochRegistry is a passive
record — it doesn't trigger ceremonies, it records their results.
Option A) On-chain signal only: triggerEmergencyRotation() emits an EmergencyRotationTriggered event and sets a boolean
emergencyRotationRequested. The gateway's EpochManager watches for this event and triggers an immediate refresh, bypassing the timer.
After the refresh completes, gateway calls registerEpoch as normal.
Option B) On-chain invalidation: triggerEmergencyRotation() immediately marks the current epoch as expired (sets currentEpochId to 0 or a
sentinel). isEpochActive returns false for everything. This forces all privacy tasks to fail until a new epoch is registered — a hard
kill switch.
Option C) Drop it: emergency rotation is an operational concern, not a contract concern. The admin can just call the gateway's
newt_dkg_initiate RPC to start a new ceremony, or restart the EpochManager with a shorter interval. No on-chain action needed.
My recommendation: Option A — on-chain signal. Reasons:
- It's a lightweight event + flag (~20k gas), not a complex state change
- Gives the admin a single transaction to trigger rotation from a Safe multisig, without needing gateway RPC access
- The gateway already watches on-chain events (chain watcher pattern) — watching EmergencyRotationTriggered is natural
- Doesn't break active privacy tasks (unlike Option B which is a DoS on the system)
- Gateway clears the flag by registering the next epoch
bool public emergencyRotationRequested;
function triggerEmergencyRotation() external onlyOwner {
emergencyRotationRequested = true;
emit EmergencyRotationTriggered(currentEpochId, msg.sender);
}
Gateway clears it in registerEpoch:
if (emergencyRotationRequested) {
emergencyRotationRequested = false;
}
A, B, or C?
❯ A.
⏺ Question 8 of N: Should registerEpoch enforce monotonicity and duplicate prevention?
When the gateway calls registerEpoch, we need to prevent:
- Duplicate registration: same epochId registered twice (replay or bug)
- Out-of-order registration: epoch 5 registered before epoch 4
- Gap skipping: epoch 3 registered after epoch 1 (skipping 2)
My recommendation: Enforce strict monotonicity — epochId == currentEpochId + 1 (or epochId == 0 for the first epoch). This is a single
check that prevents all three issues:
function registerEpoch(EpochInfo calldata info) external {
require(operatorRegistry.isTaskGenerator(msg.sender), NotTaskGenerator());
if (currentEpochId == 0 && epochs[0].startBlock == 0) {
// First epoch registration
require(info.epochId == 0, InvalidEpochId());
} else {
require(info.epochId == currentEpochId + 1, InvalidEpochId());
}
epochs[info.epochId] = info;
currentEpochId = info.epochId;
if (emergencyRotationRequested) {
emergencyRotationRequested = false;
}
emit EpochRegistered(info.epochId, info.mpk, info.threshold, info.committeeSize);
}
One subtlety: should the first epoch be epochId == 0 or epochId == 1? The Rust side uses current_epoch + 1 starting from 0, so the first
real epoch after initial DKG would be epoch 1. But the initial DKG ceremony itself could be epoch 0.
My recommendation: First epoch is 0. The initial FROST DKG produces epoch 0. PSS refresh produces epoch 1, 2, etc. This matches
EpochManager.current_epoch starting at 0.
Agree with strict monotonicity and epoch-0 start?
❯ yes
⏺ Question 9 of N: Cross-chain deployment — source only, or source + destination?
The existing registries have a clear pattern:
- IdentityRegistry and ConfidentialDataRegistry are deployed on all chains (source + destination) because operators on dest chains need
to call them - OperatorRegistry lives on the source chain only — dest chains get operator state via the transporter
For EpochRegistry, the consumers are:
- Gateway — calls registerEpoch after PSS refresh (one gateway, serves all chains)
- PrivacySlasher (future, NEWT-640) — needs isEpochActive and getEpoch to verify slashing claims
- Operators — don't call the contract directly (they get epoch state from the gateway via RPC)
The gateway submits transactions to the source chain (where isTaskGenerator is enforced). The PrivacySlasher would also live on the
source chain (where EigenLayer staking and slashing infrastructure exists).
Option A) Source chain only: EpochRegistry deployed alongside OperatorRegistry on the source chain. PrivacySlasher on source chain can
read it directly. Dest chains don't need epoch state — they verify BLS certificates via BN254CertificateVerifier, not epoch checks.
Option B) Source + destination: Deploy on all chains via NewtonCrossChainDeploymentLib. Dest chain PrivacySlashers could reference local
epoch state. But we don't have dest chain slashers planned — slashing goes through the source chain.
My recommendation: Option A — source chain only. No consumer on destination chains needs epoch data. The PrivacySlasher (NEWT-640) and
DleqVerifier (NEWT-641) will live on the source chain. If a future dest-chain slasher needs epoch data, we can add cross-chain relay at
that point. Avoids unnecessary deployment complexity and transporter sync overhead.
A or B?
❯ A
⏺ Question 10 of N: operatorSetHash computation — who computes it and what's the canonical encoding?
The operatorSetHash is keccak256 of the participating operators. This needs to be deterministic so the PrivacySlasher can recompute it
from an operator list and verify it matches what's stored.
Two sub-questions:
╭─── Claude Code v2.1.105 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │ Recent activity │
│ Welcome back Dennis! │ 12s ago (session) │
│ │ 1m ago rebase to pulled main branch │
│ │ 3h ago (session) │
│ ▐▛███▜▌ │ /resume for more │
│ ▝▜█████▛▘ │ ────────────────────────────────────────────────────────────────────────────────── │
│ ▘▘ ▝▝ │ What's new │
│ │ Added path parameter to the EnterWorktree tool to switch into an existing wor… │
│ Opus 4.6 (1M context) · API Usage Billing · Dennis │ Added PreCompact hook support: hooks can now block compaction by exiting with cod… │
│ Won │ Added background monitor support for plugins via a top-level monitors manifest … │
│ ~/projects/newton-prover-avs │ /release-notes for more │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
⚠ Large .claude/rules/lessons.md will impact performance (47.3k chars > 40.0k) · /memory to edit
⏺ Now update the phase status table and remaining sections:
Wrote 1 memory (ctrl+o to expand)
⏺ Now update MEMORY.md index:
Searched for 1 pattern, recalled 1 memory, searched memories (ctrl+o to expand)
⏺ Now update the MEMORY.md Privacy Layer section:
Wrote 1 memory (ctrl+o to expand)
⏺ Now save to supermemory:
⏺ Bash(node "$HOME/.claude/plugins/cache/supermemory-plugins/claude-supermemory/0.0.2/scripts/save-project-memory.cjs"
"[SAVE:dennis:2026-04-14]…)
⎿ Project knowledge saved: newton-prover-avs
ID: gev1hXQFoWB24ZSgT22Tsa
⏺ Now save to recall MCP:
⏺ Recall MCP connection is down — this happens intermittently with the Redis-backed service. Local memory and supermemory saves were
successful. Let me confirm the summary.
Save complete (2 of 3 systems):
┌──────────────┬─────────┬───────────────────────────────────────────────────────────────────────────┐
│ System │ Status │ Details │
├──────────────┼─────────┼───────────────────────────────────────────────────────────────────────────┤
│ Local memory │ Updated │ privacy-layer.md — phase table, merged PRs, remaining work, new learnings │
├──────────────┼─────────┼───────────────────────────────────────────────────────────────────────────┤
│ Supermemory │ Saved │ ID gev1hXQFoWB24ZSgT22Tsa — team-visible progress update │
├──────────────┼─────────┼───────────────────────────────────────────────────────────────────────────┤
│ Recall MCP │ Skipped │ Connection closed — can retry next session │
└──────────────┴─────────┴───────────────────────────────────────────────────────────────────────────┘
Key changes in local memory:
- Phase 2C status: IN PROGRESS → MERGED
- Gateway-never-decrypts + multi-domain: IN PROGRESS → MERGED
- All 6 PRs moved from "Open PRs" to "Merged PRs" table
- NEWT-628 moved from "Remaining Work" to "Completed Tickets"
- Added 4 new learnings (Arc receiver, cascading gates, slither config, RPC helper dedup)
- Bold callout: "All Privacy Layer Rust code complete — remaining work is Solidity only"
✻ Churned for 2m 55s
❯ Now let's move on to the next steps for Newton Privacy Layer
⏺ ★ Insight ─────────────────────────────────────
With all Rust code merged (Phases 1→2C), the Privacy Layer transitions from cryptographic infrastructure to on-chain enforcement. The
remaining work is Solidity contracts that make epoch rotation and privacy slashing verifiable and enforceable on-chain — closing the
trust loop.
─────────────────────────────────────────────────
Based on the remaining backlog, here are the 5 tickets ordered by dependency and priority:
┌──────────┬──────────┬────────────────────────────────────┬──────────────────┐
│ Priority │ Ticket │ Title │ Blocked by │
├──────────┼──────────┼────────────────────────────────────┼──────────────────┤
│ 1 │ NEWT-639 │ EpochRegistry contract │ None — ready now │
├──────────┼──────────┼────────────────────────────────────┼──────────────────┤
│ 2 │ NEWT-630 │ On-chain aggregation commitments │ None — ready now │
├──────────┼──────────┼────────────────────────────────────┼──────────────────┤
│ 3 │ NEWT-629 │ Privacy slashing conditions (spec) │ None — ready now │
├──────────┼──────────┼────────────────────────────────────┼──────────────────┤
│ 4 │ NEWT-640 │ PrivacySlasher contract │ NEWT-629 │
├──────────┼──────────┼────────────────────────────────────┼──────────────────┤
│ 5 │ NEWT-641 │ DleqVerifier library │ NEWT-629 │
└──────────┴──────────┴────────────────────────────────────┴──────────────────┘
NEWT-639 (EpochRegistry) is the natural next step — it's the on-chain counterpart to the PSS infrastructure we just merged. The
EpochManager already writes epoch metadata locally; this contract makes it verifiable on-chain (MPK, operator set hash, threshold,
timestamps).
NEWT-629 + NEWT-640 + NEWT-641 form the slashing subsystem — the DleqVerifier library feeds into the PrivacySlasher contract, which
implements the slashing conditions spec.
Which ticket do you want to start with? Or would you prefer to do a /grill-me on the EpochRegistry contract design first?
✻ Churned for 31s
❯ yes /grill-me for EpochRegistry for NEWT-639
⏺ Skill(grill-me)
⎿ Successfully loaded skill
⏺ Let me first gather context on the existing contract patterns and the NEWT-639 spec before starting the grill.
Called linear-server (ctrl+o to expand)
⏺ Now let me check existing contract patterns — proxy setup, access control, and similar registries we already have.
⏺ Explore(Explore EpochRegistry contract context)
⎿ Done (33 tool uses · 80.4k tokens · 1m 45s)
(ctrl+o to expand)
⏺ Good context gathered. Now let me start the grill.
Question 1 of N: Proxy vs Non-Proxy Deployment
The existing registries (IdentityRegistry, ConfidentialDataRegistry, PolicyClientRegistry) all use TransparentUpgradeableProxy with
OwnableUpgradeable. However, BatchTaskManager is deployed without a proxy (lessons.md: "not proxied — redeploy is the only upgrade
path").
For EpochRegistry, the decision matters because:
- Proxied: upgradeable, keeps address stable across versions, but adds gas overhead and storage layout constraints
- Non-proxied: simpler, cheaper gas, but requires redeployment + re-authorization to upgrade
The EpochRegistry stores epoch history (MPK, operator set hashes, destruction attestations). If we redeploy, we lose that history unless
we migrate it.
My recommendation: Proxy (TransparentUpgradeableProxy). The epoch history is valuable for auditability and slashing disputes — losing it
on upgrade is unacceptable. The PrivacySlasher (NEWT-640) will need to reference historical epochs to verify slashing claims. Same
pattern as the other registries.
Do you agree with proxied deployment, or should this be non-proxied like BatchTaskManager?
✻ Brewed for 2m 33s
❯ agree
⏺ Question 2 of N: Who can call registerEpoch?
The spec says "Gateway/aggregator only (task generator role)." In practice, the EpochManager in the gateway drives PSS refresh and would
call registerEpoch after a successful ceremony. Let me check who calls what in the existing pattern:
- IdentityRegistry.registerIdentityData — requires gateway EIP-712 co-signature, verified via
operatorRegistry.isTaskGenerator(recoveredSigner) - ConfidentialDataRegistry.publishData — permissionless (any registered provider)
- NewtonProverTaskManager.createTask — checks isTaskGenerator(msg.sender)
For EpochRegistry, the caller is the gateway's EpochManager, which signs transactions with task_generator_signer (same ECDSA key used for
task creation and DKG ceremonies).
Two options:
- A) require(operatorRegistry.isTaskGenerator(msg.sender)) — simple, direct msg.sender check. Same pattern as createTask. Gateway calls
directly. - B) EIP-712 signature verification — gateway signs off-chain, anyone can relay. More complex but enables gasless submission by a
relayer.
My recommendation: Option A — direct isTaskGenerator(msg.sender). Epoch registration is a low-frequency operation (once per epoch,
default 24h). There's no need for relayer patterns. The gateway already has the signer key and submits on-chain transactions for task
responses. Keeps the contract simpler and cheaper.
A or B?
❯ A
⏺ Question 3 of N: Should EpochRegistry take operatorRegistry as a constructor immutable or as an initializer parameter?
Looking at the existing pattern:
- IdentityRegistry takes operatorRegistry and policyClientRegistry as immutable constructor parameters (set at implementation deploy
time, not in initialize()) - ConfidentialDataRegistry takes policyClientRegistry as an immutable constructor parameter
- These are immutable because the registry addresses don't change — if they did, you'd redeploy the implementation and upgrade the proxy
For EpochRegistry, we need operatorRegistry to call isTaskGenerator(msg.sender). The operator registry address is stable per chain
(deployed once, upgraded in place).
My recommendation: Immutable constructor parameter, matching IdentityRegistry/ConfidentialDataRegistry pattern:
contract EpochRegistry is Initializable, OwnableUpgradeable {
IOperatorRegistry public immutable operatorRegistry;
constructor(IOperatorRegistry _operatorRegistry) {
operatorRegistry = _operatorRegistry;
_disableInitializers();
}
function initialize(address owner) external initializer {
__Ownable_init(owner);
}
}
Agree?
❯ yes
⏺ Question 4 of N: What fields belong in EpochInfo and what's the on-chain storage model?
The spec proposes:
struct EpochInfo {
uint64 epochId;
bytes32 mpk; // Master Public Key (X25519, 32 bytes)
bytes32 operatorSetHash; // keccak256 of participating operator addresses
uint8 threshold; // t in t-of-n
uint8 committeeSize; // n
uint256 startBlock;
uint256 endBlock; // 0 if current epoch
uint256 dkgCeremonyBlock;
}
A few design questions nested here:
MPK format: The Rust side uses Edwards Y (compressed, 32 bytes) which converts to X25519 for HPKE. Storing bytes32 is fine — consumers
interpret it. But should we store the Edwards point or the X25519 point? The PrivacySlasher (NEWT-640) will need to verify DLEQ proofs
against operator public shares, which use Edwards. Clients encrypt to X25519.
My recommendation: Store Edwards Y (bytes32). The X25519 conversion is deterministic and can be done off-chain. DLEQ verification
(NEWT-641) needs the Edwards form. Add a comment clarifying the encoding.
endBlock: The spec uses 0 for "current epoch" and sets it when a new epoch registers. But this means registerEpoch for epoch N+1 must
also write to epoch N's storage slot (to set endBlock). That's an extra SSTORE (~5k gas for warm slot).
My recommendation: Drop endBlock. The end of epoch N is implicitly startBlock of epoch N+1. One less storage write, and isEpochActive can
compute it from the next epoch's startBlock. Store a currentEpochId state variable instead — getEpoch(currentEpochId) gives the active
epoch.
dkgCeremonyBlock: When is this useful? The DKG/PSS ceremony happens off-chain. This block number would be the block at which
registerEpoch was called, which is just startBlock. Seems redundant.
My recommendation: Drop dkgCeremonyBlock. If we need ceremony provenance, the transaction hash of registerEpoch is sufficient.
Proposed trimmed struct:
struct EpochInfo {
uint64 epochId;
bytes32 mpk; // Compressed Edwards Y of threshold master public key
bytes32 operatorSetHash; // keccak256(abi.encodePacked(sorted operator addresses))
uint8 threshold; // t in t-of-n
uint8 committeeSize; // n
uint256 startBlock; // block.number at registration
}
Plus state: uint64 public currentEpochId;
Agree with trimming endBlock and dkgCeremonyBlock?
❯ agree
⏺ Question 5 of N: Grace period — block-based or time-based?
The spec says "configurable duration (default: 600 blocks ~2h)" and the Rust side uses grace_period_epochs: u32 (number of past epochs to
keep, default 2).
These are two different models:
- A) Block-based grace: epoch N enters grace period when epoch N+1 registers, expires after gracePeriodBlocks blocks. Requires
isEpochActive to check block.number < epochs[epochId].startBlock + gracePeriodBlocks. But block times vary per chain (12s L1, 2s L2) —
600 blocks is ~2h on L1 but ~20min on L2. - B) Epoch-count grace: epoch N is active if currentEpochId - epochId <= gracePeriodEpochs. Simple integer comparison. Chain-agnostic.
Matches the Rust side's grace_period_epochs config. - C) Timestamp-based grace: use block.timestamp instead of block.number. Chain-agnostic but block.timestamp is manipulable by validators
(within bounds).
My recommendation: Option B — epoch-count grace. Reasons:
- Matches the Rust side's existing model (grace_period_epochs: 2)
- Chain-agnostic — no time-normalization needed per chain
- Simplest check: currentEpochId - epochId <= gracePeriodEpochs
- The PrivacySlasher needs to know "is this epoch still valid for slashing?" — epoch-count is the cleanest predicate
- Store gracePeriodEpochs as a configurable uint8 (owner-settable)
isEpochActive(epochId) becomes:
function isEpochActive(uint64 epochId) external view returns (bool) {
if (epochId > currentEpochId || epochs[epochId].startBlock == 0) return false;
return currentEpochId - epochId <= gracePeriodEpochs;
}
A, B, or C?
❯ B
⏺ Question 6 of N: Destruction attestations — do we need them on-chain?
The spec includes recordDestructionAttestation(epochId, operator, signature) — operators attest that they've destroyed their old epoch
key shares. This is a forward-secrecy signal: "I no longer hold shares for epoch N."
But thinking through the threat model:
- Honest operator: destroys shares after grace period, may or may not bother submitting an on-chain attestation (costs gas)
- Malicious operator: retains shares regardless — an on-chain attestation proving destruction is unforgeable in theory, but an operator
can sign the attestation and still keep the shares. There's no cryptographic proof of deletion. - Verification: nobody can verify that shares were actually destroyed. The attestation is a trust signal, not a proof.
Each attestation is one SSTORE per operator per epoch. With 20 operators and epoch-count grace of 2, that's 20 transactions every ~24h
just for attestations that can't be verified.
Three options:
- A) Keep on-chain attestations: trust signal for compliance/audit, even if unverifiable. PrivacySlasher could require attestation before
releasing operator from epoch liability. - B) Off-chain attestations only: operators sign attestations, gateway stores them. Available via RPC for auditors. Zero gas cost.
- C) Drop entirely: old shares are overwritten by PSS refresh (the accumulate_refresh replaces the share in memory + keystore). The
protocol already enforces rotation — explicit destruction attestation adds ceremony without security.
My recommendation: Option C — drop destruction attestations. PSS refresh inherently replaces shares. The keystore delete_epoch_keystore
already removes old files after grace period. Adding unverifiable on-chain attestations is compliance theater that costs gas without
improving security. If auditors need proof of rotation, the EpochRegistered events form a chain of epoch transitions — that's the
verifiable signal.
A, B, or C?
❯ C
⏺ Question 7 of N: Emergency rotation — what should it actually do?
The spec includes triggerEmergencyRotation() (admin only). The question is what this does on-chain vs off-chain:
The actual PSS refresh ceremony is entirely off-chain (gateway coordinates operators via RPC). The on-chain EpochRegistry is a passive
record — it doesn't trigger ceremonies, it records their results.
Option A) On-chain signal only: triggerEmergencyRotation() emits an EmergencyRotationTriggered event and sets a boolean
emergencyRotationRequested. The gateway's EpochManager watches for this event and triggers an immediate refresh, bypassing the timer.
After the refresh completes, gateway calls registerEpoch as normal.
Option B) On-chain invalidation: triggerEmergencyRotation() immediately marks the current epoch as expired (sets currentEpochId to 0 or a
sentinel). isEpochActive returns false for everything. This forces all privacy tasks to fail until a new epoch is registered — a hard
kill switch.
Option C) Drop it: emergency rotation is an operational concern, not a contract concern. The admin can just call the gateway's
newt_dkg_initiate RPC to start a new ceremony, or restart the EpochManager with a shorter interval. No on-chain action needed.
My recommendation: Option A — on-chain signal. Reasons:
- It's a lightweight event + flag (~20k gas), not a complex state change
- Gives the admin a single transaction to trigger rotation from a Safe multisig, without needing gateway RPC access
- The gateway already watches on-chain events (chain watcher pattern) — watching EmergencyRotationTriggered is natural
- Doesn't break active privacy tasks (unlike Option B which is a DoS on the system)
- Gateway clears the flag by registering the next epoch
bool public emergencyRotationRequested;
function triggerEmergencyRotation() external onlyOwner {
emergencyRotationRequested = true;
emit EmergencyRotationTriggered(currentEpochId, msg.sender);
}
Gateway clears it in registerEpoch:
if (emergencyRotationRequested) {
emergencyRotationRequested = false;
}
A, B, or C?
❯ A.
⏺ Question 8 of N: Should registerEpoch enforce monotonicity and duplicate prevention?
When the gateway calls registerEpoch, we need to prevent:
- Duplicate registration: same epochId registered twice (replay or bug)
- Out-of-order registration: epoch 5 registered before epoch 4
- Gap skipping: epoch 3 registered after epoch 1 (skipping 2)
My recommendation: Enforce strict monotonicity — epochId == currentEpochId + 1 (or epochId == 0 for the first epoch). This is a single
check that prevents all three issues:
function registerEpoch(EpochInfo calldata info) external {
require(operatorRegistry.isTaskGenerator(msg.sender), NotTaskGenerator());
if (currentEpochId == 0 && epochs[0].startBlock == 0) {
// First epoch registration
require(info.epochId == 0, InvalidEpochId());
} else {
require(info.epochId == currentEpochId + 1, InvalidEpochId());
}
epochs[info.epochId] = info;
currentEpochId = info.epochId;
if (emergencyRotationRequested) {
emergencyRotationRequested = false;
}
emit EpochRegistered(info.epochId, info.mpk, info.threshold, info.committeeSize);
}
One subtlety: should the first epoch be epochId == 0 or epochId == 1? The Rust side uses current_epoch + 1 starting from 0, so the first
real epoch after initial DKG would be epoch 1. But the initial DKG ceremony itself could be epoch 0.
My recommendation: First epoch is 0. The initial FROST DKG produces epoch 0. PSS refresh produces epoch 1, 2, etc. This matches
EpochManager.current_epoch starting at 0.
Agree with strict monotonicity and epoch-0 start?
❯ yes
⏺ Question 9 of N: Cross-chain deployment — source only, or source + destination?
The existing registries have a clear pattern:
- IdentityRegistry and ConfidentialDataRegistry are deployed on all chains (source + destination) because operators on dest chains need
to call them - OperatorRegistry lives on the source chain only — dest chains get operator state via the transporter
For EpochRegistry, the consumers are:
- Gateway — calls registerEpoch after PSS refresh (one gateway, serves all chains)
- PrivacySlasher (future, NEWT-640) — needs isEpochActive and getEpoch to verify slashing claims
- Operators — don't call the contract directly (they get epoch state from the gateway via RPC)
The gateway submits transactions to the source chain (where isTaskGenerator is enforced). The PrivacySlasher would also live on the
source chain (where EigenLayer staking and slashing infrastructure exists).
Option A) Source chain only: EpochRegistry deployed alongside OperatorRegistry on the source chain. PrivacySlasher on source chain can
read it directly. Dest chains don't need epoch state — they verify BLS certificates via BN254CertificateVerifier, not epoch checks.
Option B) Source + destination: Deploy on all chains via NewtonCrossChainDeploymentLib. Dest chain PrivacySlashers could reference local
epoch state. But we don't have dest chain slashers planned — slashing goes through the source chain.
My recommendation: Option A — source chain only. No consumer on destination chains needs epoch data. The PrivacySlasher (NEWT-640) and
DleqVerifier (NEWT-641) will live on the source chain. If a future dest-chain slasher needs epoch data, we can add cross-chain relay at
that point. Avoids unnecessary deployment complexity and transporter sync overhead.
A or B?
❯ A
⏺ Question 10 of N: operatorSetHash computation — who computes it and what's the canonical encoding?
The operatorSetHash is keccak256 of the participating operators. This needs to be deterministic so the PrivacySlasher can recompute it
from an operator list and verify it matches what's stored.
Two sub-questions:
Who computes it?
- Off-chain (gateway): gateway computes the hash and passes it in EpochInfo. Contract stores it blindly. Simpler contract, but the
contract trusts the gateway's hash. - On-chain (contract): gateway passes the raw operator address array, contract sorts and hashes. More gas, but the hash is verifiable
on-chain.
My recommendation: Off-chain. The gateway already knows the operator set (it built operator_sockets for the PSS ceremony). The
registerEpoch caller is already trusted (isTaskGenerator). Having the contract re-sort and hash an array adds gas for no security benefit
— the task generator is trusted to submit correct data.
Canonical encoding?
The hash must be reproducible. Two common patterns:
- keccak256(abi.encodePacked(sortedAddresses)) — compact, 20 bytes per address
- keccak256(abi.encode(sortedAddresses)) — ABI-encoded with padding, 32 bytes per address
My recommendation: abi.encodePacked with addresses sorted ascending. It's the more gas-efficient encoding and matches how operator sets
are typically hashed in EigenLayer middleware. Document the sort requirement in the interface NatSpec.
// Off-chain: gateway computes before calling registerEpoch
// bytes32 hash = keccak256(abi.encodePacked(sortedOperatorAddresses));
Agree with off-chain computation + abi.encodePacked of sorted addresses?
❯ Agreed
⏺ Question 11 of N: Should registerEpoch validate that mpk changed (or didn't change)?
PSS refresh preserves the MPK — that's the whole point. Resharing also preserves it. So mpk should be identical across all epochs unless
the initial DKG is re-run from scratch (which would be a new master secret, breaking all existing encrypted data).
Three options:
- A) Enforce MPK immutability: require(info.mpk == epochs[currentEpochId].mpk) for all epochs after 0. Catches bugs where a gateway
accidentally registers with a different MPK. - B) Allow MPK change: store whatever the gateway sends. Flexibility for re-keying scenarios (e.g., full operator set replacement where
resharing isn't possible). - C) Enforce MPK immutability with admin override: default enforce, but setMpk(bytes32) by admin for re-keying scenarios.
My recommendation: Option A with a carve-out for epoch 0. The MPK is the anchor — if it changes, all previously encrypted data becomes
undecryptable. This is a critical invariant. If we ever need to re-key (catastrophic compromise), that's a fresh DKG which should go
through a deliberate admin process — not something that happens silently via registerEpoch. We can add an admin resetToNewMpk() later if
needed.
if (currentEpochId > 0 || epochs[0].startBlock != 0) {
// Not the first epoch — MPK must be preserved
require(info.mpk == epochs[currentEpochId].mpk, MpkMismatch());
}
A, B, or C?
❯ A.
Question 12 of N: Configurable parameters — what's owner-settable after deployment?
We have one configurable parameter so far: gracePeriodEpochs. Let me think through if anything else needs to be tunable post-deployment.
┌────────────────────────────┬────────────────────────┬───────────────────────────────────┬──────────────────────────────────────────┐
│ Parameter │ Set at deploy │ Owner-settable │ Rationale │
├────────────────────────────┼────────────────────────┼───────────────────────────────────┼──────────────────────────────────────────┤
│ operatorRegistry │ Constructor │ No │ Stable address, upgrade via proxy │
│ │ (immutable) │ │ redeploy │
├────────────────────────────┼────────────────────────┼───────────────────────────────────┼──────────────────────────────────────────┤
│ gracePeriodEpochs │ initialize() │ Yes │ May need tuning based on operator │
│ │ │ │ behavior │
├────────────────────────────┼────────────────────────┼───────────────────────────────────┼──────────────────────────────────────────┤
│ emergencyRotationRequested │ N/A │ Yes (via │ Already decided │
│ │ │ triggerEmergencyRotation) │ │
└────────────────────────────┴────────────────────────┴───────────────────────────────────┴──────────────────────────────────────────┘
I don't see other parameters that need runtime tuning. The threshold, committeeSize, and operatorSetHash are per-epoch (stored in
EpochInfo), not global config.
My recommendation: Single setter:
uint8 public gracePeriodEpochs;
function setGracePeriodEpochs(uint8 _gracePeriodEpochs) external onlyOwner {
gracePeriodEpochs = _gracePeriodEpochs;
emit GracePeriodUpdated(_gracePeriodEpochs);
}
Default value set in initialize() (e.g., 2). That's it — no other knobs needed.
Agree, or are there other parameters that should be tunable?
❯ Agree, or are there any scenarios that we need something tunable?
⏺ I've thought through potential scenarios:
- Epoch duration: lives off-chain in EpochConfig.duration_seconds — the contract doesn't enforce timing between epochs, it just records
what happened - MPK override for re-keying: decided to enforce immutability (Q11) — if we need re-keying later, add a dedicated admin function
- Max committee size: could cap committeeSize to prevent gas issues in future slashing loops, but the permissioned operator set is
already small (~20 max). Not worth the complexity now.
No additional tunables needed. Just gracePeriodEpochs.
Question 13 of N: Gas budget and storage layout
The spec targets registerEpoch < 80k gas. Let me estimate:
EpochInfo storage (per epoch):
epochId (uint64) — packed
threshold (uint8) — packed with epochId
committeeSize (uint8) — packed with epochId
mpk (bytes32) — 1 slot
operatorSetHash (bytes32)— 1 slot
startBlock (uint256) — 1 slot
That's 3 cold SSTOREs for a new epoch (~22,100 each = ~66k) plus currentEpochId update (warm SSTORE ~5k) plus the isTaskGenerator
external call (~2.6k) plus event emission (~1.5k). Total: ~75k gas. Tight but within 80k.
We can pack epochId (uint64) + threshold (uint8) + committeeSize (uint8) + startBlock (uint64, sufficient until year 2554) into a single
slot:
struct EpochInfo {
bytes32 mpk; // slot 0
bytes32 operatorSetHash; // slot 1
uint64 epochId; // slot 2 (packed)
uint64 startBlock; // slot 2 (packed)
uint8 threshold; // slot 2 (packed)
uint8 committeeSize; // slot 2 (packed)
}
That's 2 cold SSTOREs (~44k) + 1 cold SSTORE for the packed slot (~22k) = ~66k, leaving comfortable headroom.
My recommendation: Pack into 3 slots as shown above, using uint64 for startBlock (block numbers won't exceed 2^64). Ordering fields by
size descending for Solidity's left-packing rules. This keeps us well under the 80k gas target.
Agree with this storage layout?
⏺ I believe we've covered all the major design branches. Let me summarize the full design we've agreed on to make sure nothing was missed.
EpochRegistry Design Summary (13 decisions)
┌─────┬────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ # │ Decision │ Choice │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 1 │ Deployment │ TransparentUpgradeableProxy (preserves epoch history across upgrades) │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2 │ Access control │ isTaskGenerator(msg.sender) direct check for registerEpoch │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 3 │ Constructor deps │ operatorRegistry as immutable constructor param │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 4 │ EpochInfo fields │ mpk, operatorSetHash, epochId, startBlock, threshold, committeeSize — dropped endBlock and │
│ │ │ dkgCeremonyBlock │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 5 │ Grace period model │ Epoch-count based (currentEpochId - epochId <= gracePeriodEpochs) │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 6 │ Destruction │ Dropped — PSS refresh inherently replaces shares, attestations are unverifiable │
│ │ attestations │ │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 7 │ Emergency rotation │ On-chain signal flag + event, gateway watches and triggers off-chain ceremony │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 8 │ Epoch monotonicity │ Strict epochId == currentEpochId + 1, first epoch is 0 │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 9 │ Cross-chain │ Source chain only — no dest chain consumers │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 10 │ operatorSetHash │ Off-chain computed, keccak256(abi.encodePacked(sortedAddresses)) │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 11 │ MPK immutability │ Enforced — require(info.mpk == epochs[currentEpochId].mpk) after epoch 0 │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 12 │ Configurable params │ Only gracePeriodEpochs (owner-settable, default 2) │
├─────┼────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 13 │ Storage layout │ 3 slots packed — uint64 startBlock, fields ordered for packing, <80k gas │
└─────┴────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────┘
Contract skeleton:
contract EpochRegistry is Initializable, OwnableUpgradeable {
IOperatorRegistry public immutable operatorRegistry;
mapping(uint64 => EpochInfo) public epochs;
uint64 public currentEpochId;
uint8 public gracePeriodEpochs;
bool public emergencyRotationRequested;
// registerEpoch, isEpochActive, getEpoch, getCurrentEpoch
// triggerEmergencyRotation, setGracePeriodEpochs
}
Events: EpochRegistered, EmergencyRotationTriggered, GracePeriodUpdated
Not included (intentionally): destruction attestations, endBlock tracking, on-chain hash computation, dest chain deployment, EIP-712
signature auth, re-keying admin function
Anything missing or anything you'd change before we proceed to implementation?
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):
- HPKE encryption (X25519/ChaCha20Poly1305) for all three privacy paths
- Threshold DKG (FROST on Ristretto255) — operators hold key shares, MSK never assembled
- PSS epoch rotation — shares refresh every 24h, MPK constant, forward secrecy
- Gateway-never-decrypts — operators always decrypt locally
- Multi-domain resolution — identity, confidential, ephemeral all supported simultaneously
- EpochRegistry on-chain — epoch commitments, MPK immutability, grace periods
- Challenger support for privacy task replay
What's missing (Phase 2D):
- No economic penalty for operators who misbehave during threshold decryption
- No on-chain verification of partial decryption correctness
- No slashing for operators who refuse to participate
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:
- Submit garbage partial decryptions — the gateway detects this via DLEQ verification and excludes the operator, but the operator faces
zero economic consequences. They can keep doing it. - Refuse to participate — a malicious operator can selectively drop privacy tasks (censorship) with no penalty. As long as t-of-n others
participate, the task succeeds, but the attack is free. - Participate in unauthorized decryptions — if an operator's key share is compromised, there's no on-chain mechanism to prove misuse.
The practical impact:
- For users: Privacy data is protected by operator honesty, not economics. An operator could collude to reconstruct the master secret
(needs t operators) with no slashing risk. - For the protocol: Can't credibly claim "restaked security" for privacy operations. EigenLayer's value proposition is economic security
— without slashing, privacy is outside that model. - For auditors: A security audit would flag "no slashing for privacy violations" as a critical finding.
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:
- Deploy EpochRegistry to stagef/testnet (just merged)
- Run PSS epoch rotation in production for a few cycles to validate stability
- Implement Phase 2D (NEWT-629 → NEWT-641 → NEWT-640) before mainnet privacy launch
- Security audit should cover both the Rust crypto (PSS, DLEQ) and the Solidity slashing contracts together
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.
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):
- HPKE encryption (X25519/ChaCha20Poly1305) for all three privacy paths
- Threshold DKG (FROST on Ristretto255) — operators hold key shares, MSK never assembled
- PSS epoch rotation — shares refresh every 24h, MPK constant, forward secrecy
- Gateway-never-decrypts — operators always decrypt locally
- Multi-domain resolution — identity, confidential, ephemeral all supported simultaneously
- EpochRegistry on-chain — epoch commitments, MPK immutability, grace periods
- Challenger support for privacy task replay
What's missing (Phase 2D):
- No economic penalty for operators who misbehave during threshold decryption
- No on-chain verification of partial decryption correctness
- No slashing for operators who refuse to participate
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:
- Submit garbage partial decryptions — the gateway detects this via DLEQ verification and excludes the operator, but the operator faces
zero economic consequences. They can keep doing it. - Refuse to participate — a malicious operator can selectively drop privacy tasks (censorship) with no penalty. As long as t-of-n others
participate, the task succeeds, but the attack is free. - Participate in unauthorized decryptions — if an operator's key share is compromised, there's no on-chain mechanism to prove misuse.
The practical impact:
- For users: Privacy data is protected by operator honesty, not economics. An operator could collude to reconstruct the master secret
(needs t operators) with no slashing risk. - For the protocol: Can't credibly claim "restaked security" for privacy operations. EigenLayer's value proposition is economic security
— without slashing, privacy is outside that model. - For auditors: A security audit would flag "no slashing for privacy violations" as a critical finding.
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:
- Deploy EpochRegistry to stagef/testnet (just merged)
- Run PSS epoch rotation in production for a few cycles to validate stability
- Implement Phase 2D (NEWT-629 → NEWT-641 → NEWT-640) before mainnet privacy launch
- Security audit should cover both the Rust crypto (PSS, DLEQ) and the Solidity slashing contracts together
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:
- raiseAndResolveChallenge — A challenger re-evaluates the policy independently (including generating a ZK proof via SP1). If the
operator's evaluationResult was wrong, ChallengeLib.slashSigningOperators() slashes all operators who BLS-signed the incorrect response
at 10% (WADS_TO_SLASH = 100000000000000000). - challengeDirectlyVerifiedAttestation — Same mechanism for the direct attestation path.
- slashForCrossChainChallenge — Cross-chain relay of a challenge from a destination chain.
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:
- Prepare phase: Gateway asks operators for partial DH outputs. Operator A submits a wrong partial decryption with an invalid DLEQ
proof. - Gateway detects it: The Rust DLEQ verification (crates/core/src/dkg/dleq.rs) rejects Operator A's contribution. Gateway excludes
Operator A and proceeds with the remaining t-1 operators (if enough remain). - Commit phase: Policy evaluation happens with the correct decrypted data (from honest operators). BLS aggregation succeeds. The policy
result is correct. - On-chain: respondToTask lands. No policy challenge is raised (the result was 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:
- They submit a wrong partial decryption AND sign a wrong policy result
- Privacy slash hits them individually for the bad share
- Policy slash hits all signers (including them) for the bad result
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:
- The operator might be legitimately down (network issues)
- It only matters if their absence drops below threshold (n-absent < t)
- Detection is via quorum attestation (t operators attest "Operator A didn't respond"), not cryptographic proof
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:
- The challenger doesn't participate in the Prepare phase — it only sees the final respondToTask on-chain
- The DLEQ proofs are exchanged off-chain between operators and the gateway
- By the time the task response is on-chain, the bad operator was already excluded and the result is correct
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:
- Gateway posts commitmentRoot = keccak256(operator_id, D_i, pi_i) per task
- Challenger can request the individual proofs from EigenDA/gateway
- Challenger verifies each DLEQ proof independently
- If a proof is invalid, challenger submits slashing proof to PrivacySlasher
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:
- NEWT-641 (DleqVerifier library) — pure cryptographic library, no trust assumptions
- NEWT-630 (on-chain commitments) — makes DLEQ proofs publicly verifiable
- NEWT-640 (PrivacySlasher) — references both DleqVerifier and commitments
- NEWT-629 (spec update) — refine the spec to require commitment-based detection, not gateway-based
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?
⏺ 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:
- NEWT-641 — DleqVerifier library (no blockers, pure crypto)
- NEWT-630 — On-chain commitments (no blockers, extends TaskResponseMetadata)
- NEWT-640 — PrivacySlasher (blocked by both above)
- NEWT-629 — Spec finalization (refine based on implementation)
Key updates made:
- NEWT-629: Rewrote to emphasize trustless detection model, removed Tier 3 plaintext leakage (deferred to TEE), added policy-vs-privacy
slashing comparison table - NEWT-640: Updated with permissionless design, explicit DleqVerifier + commitments dependency, slashing flow for both violation types
- NEWT-641: Added blocks: NEWT-640 relation
- NEWT-630: Added blocks: NEWT-640 relation
❯ 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.
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:
- Proof translation — operator generates on Ristretto255, translates for BN254 verification
- Dual-curve commitment — operator commits public key share on both curves during DKG
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:
- c = SHA-256("newton-dleq-v1" || G || A || H || B || R1 || R2) mod order
- z = k - c * secret
- Verification: zG + cA == R1 and zH + cB == R2
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:
- We already have SP1 infrastructure (circuits/sp1-rego/) and the SP1 verifier deployed on-chain
- No changes to the DKG/PSS ceremony or key material
- The DLEQ verification is simple enough for an SP1 circuit (~100 lines)
- Gas cost is moderate (~300k) but acceptable for a slashing path (not hot path)
- The Ed25519 arithmetic runs natively inside SP1's RISC-V environment (no field translation)
- Aligns with the existing challenger pattern: challenger generates ZK proof, submits on-chain
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:
- One secret scalar s_i (a 32-byte number)
- One public share pk_i = s_i * G_ed25519 (Ed25519 point)
With dual-curve, each operator would have:
- Same secret scalar s_i (it's just a number — curve-agnostic)
- Ed25519 public share: pk_i_ed = s_i * G_ed25519
- BN254 public share: pk_i_bn = s_i * G_bn254
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:
- frost.rs round3: after computing s_i, also compute s_i * G_bn254 and store it
- keystore.rs: store both public shares in the encrypted keystore
- types.rs: add bn254_public_share: [u8; 64] to KeyShare (BN254 points are 64 bytes uncompressed)
- FROST package broadcast: include the BN254 public share in round3 output so all parties know it
~50 lines of code change in the DKG crate. Not trivial but not massive.
PSS refresh changes:
- refresh.rs: Feldman commitments are currently Ed25519 only. Would need BN254 commitments too (or derive from the same polynomial, since
coefficients are scalars) - Actually — the polynomial coefficients are scalars, and Feldman verification is share * G == sum(C_j * x^j). If we compute Feldman
commitments on both curves, verification works on both independently - ~30 lines additional in refresh.rs
Operator runtime changes:
- During Prepare phase: operator generates DLEQ proof on BN254 instead of (or in addition to) Ed25519
- Requires a BN254 scalar multiplication library in Rust (e.g., ark-bn254 or alloy's BN254 support)
- New dependency in the operator crate
On-chain changes:
- EpochRegistry or a companion contract stores BN254 public shares per operator per epoch
- Additional ~21k gas per operator per epoch for storage
- DleqVerifier uses EIP-196 ecMul + ecAdd: ~12k gas for verification (very cheap)
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
- One more thing that can go wrong during DKG: BN254 key derivation failure
- One more field to serialize/deserialize in keystore
- One more commitment to verify during PSS refresh
- But all of these are deterministic — if the scalar is valid (which it is by construction), the BN254 operations can't fail
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:
- Modifying the DKG ceremony is high-stakes — FROST is the most complex crypto in the codebase. Any bug in round3 BN254 key derivation
could produce invalid key material across all operators, requiring a fresh DKG ceremony to recover - PSS refresh with dual Feldman commitments doubles the verification surface — a bug means operators silently accumulate invalid BN254
shares across epochs - The "scalar valid in both group orders" invariant is a latent footgun that every future contributor must know about
- New ark-bn254 dependency in the operator crate — different serialization format, different point representation, new attack surface
- Every operator must upgrade simultaneously to support BN254 — partial rollout breaks the ceremony
SP1 (B) execution risks:
- The DLEQ verification logic is ~30 lines of Rust (same code as dleq.rs::verify(), compiled to RISC-V)
- No changes to DKG, PSS, keystore, or operator runtime
- No new dependencies — curve25519-dalek already works in SP1's RISC-V target
- The SP1 verifier contract is already deployed and battle-tested on-chain
- Slashing is rare (dispute path, not hot path) — 10-30s proof generation is acceptable
- If the SP1 circuit has a bug, fix and redeploy the circuit — no operator coordination needed
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:
- Take in all DLEQ proofs for a task
- Verify each against the operator's committed public share
- Output which operators failed
Public inputs (committed on-chain, verifiable by anyone):
- decryptionCommitment (bytes32) — from NEWT-630, the on-chain commitment hash
- epochId (uint64) — which epoch's key shares to verify against
- taskId (bytes32) — which task's partial decryptions
Private inputs (provided by the challenger, not on-chain):
- Per-operator: (operator_index, pk_share, enc_point, partial_decryption, dleq_proof)
- These are fetched from EigenDA or the gateway's data availability layer
Public outputs (written to the proof, verified on-chain):
- invalidOperatorBitmap (uint256) — bitmask of which operators submitted invalid DLEQ proofs
- commitmentHash (bytes32) — re-computed from the private inputs, must match the on-chain decryptionCommitment
The on-chain PrivacySlasher then:
- Verifies the SP1 proof
- Checks commitmentHash == stored decryptionCommitment (binding)
- Slashes each operator flagged in invalidOperatorBitmap
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:
- The emergency signal is a speed-up, not a gate. The EpochManager already rotates on a timer (default 24h). An Ethereum halt delays the
emergency rotation, not all rotation. Scheduled rotations are purely off-chain (operator RPC calls) and don't depend on Ethereum being
live. - The halt also blocks the attacker. If Ethereum is halted, the compromised key share can't be exploited on-chain either — no
transactions can land. The privacy data is only useful in the context of on-chain task submissions, which are also blocked. - The actual blast radius is: time between the admin detecting the compromise and the next scheduled refresh, which is at most
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:
- DAO-compatible — governance contracts can only interact via on-chain tx
- Auditable — when admin requested rotation vs when it completed is visible on-chain
- No network access needed — admin (Safe multisig) doesn't need gateway RPC endpoint
- Immutable record — can't be disputed or lost
Cons:
- Latency tied to block time (2-24s depending on chain)
- Publicly broadcasts that an emergency occurred (operational info leak)
- Ethereum halt delays the signal (falls back to scheduled rotation timer)
- Gas cost for the tx (minimal, ~27k)
Off-chain emergency signal (admin RPC endpoint on gateway)
Pros:
- Instant — no block time latency, sub-second
- Private — no public broadcast of the emergency
- Works during chain halt
- Zero gas cost
Cons:
- Not DAO-compatible — governance contracts can't call an HTTP endpoint
- Requires admin to have network access to the gateway (VPN, internal network)
- No immutable audit trail — log-based only, can be tampered
- Requires separate auth mechanism for the admin endpoint (API key, mTLS)
- Gateway being down/unreachable blocks the emergency signal entirely
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):
- NEWT-639 (EpochRegistry) — Done, PR #544 attached
- NEWT-628 (PSS epoch rotation) — Done
- All Phase 1→2C tickets — Done
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):
- HPKE encryption (X25519/ChaCha20Poly1305) for all three privacy paths
- Threshold DKG (FROST on Ristretto255) — operators hold key shares, MSK never assembled
- PSS epoch rotation — shares refresh every 24h, MPK constant, forward secrecy
- Gateway-never-decrypts — operators always decrypt locally
- Multi-domain resolution — identity, confidential, ephemeral all supported simultaneously
- EpochRegistry on-chain — epoch commitments, MPK immutability, grace periods
- Challenger support for privacy task replay
What's missing (Phase 2D):
- No economic penalty for operators who misbehave during threshold decryption
- No on-chain verification of partial decryption correctness
- No slashing for operators who refuse to participate
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:
- Submit garbage partial decryptions — the gateway detects this via DLEQ verification and excludes the operator, but the operator faces
zero economic consequences. They can keep doing it. - Refuse to participate — a malicious operator can selectively drop privacy tasks (censorship) with no penalty. As long as t-of-n others
participate, the task succeeds, but the attack is free. - Participate in unauthorized decryptions — if an operator's key share is compromised, there's no on-chain mechanism to prove misuse.
The practical impact:
- For users: Privacy data is protected by operator honesty, not economics. An operator could collude to reconstruct the master secret
(needs t operators) with no slashing risk. - For the protocol: Can't credibly claim "restaked security" for privacy operations. EigenLayer's value proposition is economic security
— without slashing, privacy is outside that model. - For auditors: A security audit would flag "no slashing for privacy violations" as a critical finding.
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:
- Deploy EpochRegistry to stagef/testnet (just merged)
- Run PSS epoch rotation in production for a few cycles to validate stability
- Implement Phase 2D (NEWT-629 → NEWT-641 → NEWT-640) before mainnet privacy launch
- Security audit should cover both the Rust crypto (PSS, DLEQ) and the Solidity slashing contracts together
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:
- raiseAndResolveChallenge — A challenger re-evaluates the policy independently (including generating a ZK proof via SP1). If the
operator's evaluationResult was wrong, ChallengeLib.slashSigningOperators() slashes all operators who BLS-signed the incorrect response
at 10% (WADS_TO_SLASH = 100000000000000000). - challengeDirectlyVerifiedAttestation — Same mechanism for the direct attestation path.
- slashForCrossChainChallenge — Cross-chain relay of a challenge from a destination chain.
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:
- Prepare phase: Gateway asks operators for partial DH outputs. Operator A submits a wrong partial decryption with an invalid DLEQ
proof. - Gateway detects it: The Rust DLEQ verification (crates/core/src/dkg/dleq.rs) rejects Operator A's contribution. Gateway excludes
Operator A and proceeds with the remaining t-1 operators (if enough remain). - Commit phase: Policy evaluation happens with the correct decrypted data (from honest operators). BLS aggregation succeeds. The policy
result is correct. - On-chain: respondToTask lands. No policy challenge is raised (the result was 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:
- They submit a wrong partial decryption AND sign a wrong policy result
- Privacy slash hits them individually for the bad share
- Policy slash hits all signers (including them) for the bad result
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:
- The operator might be legitimately down (network issues)
- It only matters if their absence drops below threshold (n-absent < t)
- Detection is via quorum attestation (t operators attest "Operator A didn't respond"), not cryptographic proof
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:
- The challenger doesn't participate in the Prepare phase — it only sees the final respondToTask on-chain
- The DLEQ proofs are exchanged off-chain between operators and the gateway
- By the time the task response is on-chain, the bad operator was already excluded and the result is correct
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:
- Gateway posts commitmentRoot = keccak256(operator_id, D_i, pi_i) per task
- Challenger can request the individual proofs from EigenDA/gateway
- Challenger verifies each DLEQ proof independently
- If a proof is invalid, challenger submits slashing proof to PrivacySlasher
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:
- NEWT-641 (DleqVerifier library) — pure cryptographic library, no trust assumptions
- NEWT-630 (on-chain commitments) — makes DLEQ proofs publicly verifiable
- NEWT-640 (PrivacySlasher) — references both DleqVerifier and commitments
- NEWT-629 (spec update) — refine the spec to require commitment-based detection, not gateway-based
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:
- NEWT-641 — DleqVerifier library (no blockers, pure crypto)
- NEWT-630 — On-chain commitments (no blockers, extends TaskResponseMetadata)
- NEWT-640 — PrivacySlasher (blocked by both above)
- NEWT-629 — Spec finalization (refine based on implementation)
Key updates made:
- NEWT-629: Rewrote to emphasize trustless detection model, removed Tier 3 plaintext leakage (deferred to TEE), added policy-vs-privacy
slashing comparison table - NEWT-640: Updated with permissionless design, explicit DleqVerifier + commitments dependency, slashing flow for both violation types
- NEWT-641: Added blocks: NEWT-640 relation
- NEWT-630: Added blocks: NEWT-640 relation
✻ 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:
- Proof translation — operator generates on Ristretto255, translates for BN254 verification
- Dual-curve commitment — operator commits public key share on both curves during DKG
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:
- c = SHA-256("newton-dleq-v1" || G || A || H || B || R1 || R2) mod order
- z = k - c * secret
- Verification: zG + cA == R1 and zH + cB == R2
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:
- We already have SP1 infrastructure (circuits/sp1-rego/) and the SP1 verifier deployed on-chain
- No changes to the DKG/PSS ceremony or key material
- The DLEQ verification is simple enough for an SP1 circuit (~100 lines)
- Gas cost is moderate (~300k) but acceptable for a slashing path (not hot path)
- The Ed25519 arithmetic runs natively inside SP1's RISC-V environment (no field translation)
- Aligns with the existing challenger pattern: challenger generates ZK proof, submits on-chain
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:
- One secret scalar s_i (a 32-byte number)
- One public share pk_i = s_i * G_ed25519 (Ed25519 point)
With dual-curve, each operator would have:
- Same secret scalar s_i (it's just a number — curve-agnostic)
- Ed25519 public share: pk_i_ed = s_i * G_ed25519
- BN254 public share: pk_i_bn = s_i * G_bn254
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:
- frost.rs round3: after computing s_i, also compute s_i * G_bn254 and store it
- keystore.rs: store both public shares in the encrypted keystore
- types.rs: add bn254_public_share: [u8; 64] to KeyShare (BN254 points are 64 bytes uncompressed)
- FROST package broadcast: include the BN254 public share in round3 output so all parties know it
~50 lines of code change in the DKG crate. Not trivial but not massive.
PSS refresh changes:
- refresh.rs: Feldman commitments are currently Ed25519 only. Would need BN254 commitments too (or derive from the same polynomial, since
coefficients are scalars) - Actually — the polynomial coefficients are scalars, and Feldman verification is share * G == sum(C_j * x^j). If we compute Feldman
commitments on both curves, verification works on both independently - ~30 lines additional in refresh.rs
Operator runtime changes:
- During Prepare phase: operator generates DLEQ proof on BN254 instead of (or in addition to) Ed25519
- Requires a BN254 scalar multiplication library in Rust (e.g., ark-bn254 or alloy's BN254 support)
- New dependency in the operator crate
On-chain changes:
- EpochRegistry or a companion contract stores BN254 public shares per operator per epoch
- Additional ~21k gas per operator per epoch for storage
- DleqVerifier uses EIP-196 ecMul + ecAdd: ~12k gas for verification (very cheap)
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
- One more thing that can go wrong during DKG: BN254 key derivation failure
- One more field to serialize/deserialize in keystore
- One more commitment to verify during PSS refresh
- But all of these are deterministic — if the scalar is valid (which it is by construction), the BN254 operations can't fail
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:
- Modifying the DKG ceremony is high-stakes — FROST is the most complex crypto in the codebase. Any bug in round3 BN254 key derivation
could produce invalid key material across all operators, requiring a fresh DKG ceremony to recover - PSS refresh with dual Feldman commitments doubles the verification surface — a bug means operators silently accumulate invalid BN254
shares across epochs - The "scalar valid in both group orders" invariant is a latent footgun that every future contributor must know about
- New ark-bn254 dependency in the operator crate — different serialization format, different point representation, new attack surface
- Every operator must upgrade simultaneously to support BN254 — partial rollout breaks the ceremony
SP1 (B) execution risks:
- The DLEQ verification logic is ~30 lines of Rust (same code as dleq.rs::verify(), compiled to RISC-V)
- No changes to DKG, PSS, keystore, or operator runtime
- No new dependencies — curve25519-dalek already works in SP1's RISC-V target
- The SP1 verifier contract is already deployed and battle-tested on-chain
- Slashing is rare (dispute path, not hot path) — 10-30s proof generation is acceptable
- If the SP1 circuit has a bug, fix and redeploy the circuit — no operator coordination needed
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:
- Take in all DLEQ proofs for a task
- Verify each against the operator's committed public share
- Output which operators failed
Public inputs (committed on-chain, verifiable by anyone):
- decryptionCommitment (bytes32) — from NEWT-630, the on-chain commitment hash
- epochId (uint64) — which epoch's key shares to verify against
- taskId (bytes32) — which task's partial decryptions
Private inputs (provided by the challenger, not on-chain):
- Per-operator: (operator_index, pk_share, enc_point, partial_decryption, dleq_proof)
- These are fetched from EigenDA or the gateway's data availability layer
Public outputs (written to the proof, verified on-chain):
- invalidOperatorBitmap (uint256) — bitmask of which operators submitted invalid DLEQ proofs
- commitmentHash (bytes32) — re-computed from the private inputs, must match the on-chain decryptionCommitment
The on-chain PrivacySlasher then:
- Verifies the SP1 proof
- Checks commitmentHash == stored decryptionCommitment (binding)
- Slashes each operator flagged in invalidOperatorBitmap
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:
- 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?
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?
Here's the current state of the Newton Privacy Layer:
Current Status
┌──────────────────────────────┬────────────────────┬──────────────────────────────────────────────────────┐
│ Phase │ Status │ Key Deliverables │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 1: Foundation │ COMPLETE │ HPKE, Ed25519, SecureEnvelope, SDK v0.5.4 │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 1.5: Gateway-Mediated │ COMPLETE │ Gateway decryption, challenger privacy (PR #516) │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 1.5B: KMS → HPKE │ COMPLETE, DEPLOYED │ AWS KMS removed from all 5 repos │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 2 DKG core │ MERGED │ FROST DKG, Feldman VSS, keystore, ArcSwap hot-reload │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 2C: PSS epoch rotation │ IN PROGRESS │ PRs #523/#525/#526 open (~1,300 lines, 26 tests) │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 2D: Privacy slashing │ Not started │ Blocked by NEWT-628 │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 3: MPC/ZK │ Not started │ 7 backlog tickets │
├──────────────────────────────┼────────────────────┼──────────────────────────────────────────────────────┤
│ Phase 4: Ecosystem │ Not started │ 11 backlog tickets │
└──────────────────────────────┴────────────────────┴──────────────────────────────────────────────────────┘
What's Implemented in Phase 2C (PRs open, not merged)
Known Limitations
Impact: The RefreshCoordinator.run_refresh() has a TODO for HTTP fan-out to operators. The state machine, types, and operator handlers
are ready — the missing piece is the gateway driving the protocol end-to-end (collect from operators → broadcast → apply).
Next step: Wire run_refresh() with the operator pool and HTTP client from GatewayServiceImpl, following the same pattern as
DkgCoordinator.run_ceremony().
Impact: Epoch metadata (MPK, operator set hash, timestamps) exists only in-memory and keystores. No on-chain verifiability.
Next step: NEWT-639 — Solidity contract + deployer integration. Can be built in parallel since it's independent of the Rust-side work.
Impact: Operators who submit invalid partial decryptions, miss DKG rounds, or leak plaintext face no economic penalty beyond general
task misbehavior slashing.
Next step: NEWT-629 (slashing conditions), NEWT-640 (PrivacySlasher contract), NEWT-641 (DleqVerifier library). All blocked by
NEWT-628 completion.
Impact: In threshold mode, the gateway combines partial DH outputs and sees the full plaintext during task processing. A compromised
gateway can read all ephemeral privacy data.
Next step: Phase 3 — MPC computation on encrypted data without any party seeing plaintext.
Impact: Key shares from the initial DKG ceremony remain valid indefinitely. A compromised share has unlimited blast radius.
Next step: Complete the coordinator HTTP wiring in NEWT-628 to enable automatic 24h epoch rotation.
Impact: When operators are added/removed, the resharing protocol code exists but isn't triggered automatically. Currently requires
manual newt_dkg_initiate.
Next step: Wire operator set change detection in EpochManager (via chain watcher events or operator pool change callbacks) to trigger
resharing automatically.
Prioritized Next Steps
┌──────────┬─────────────────────────────────────────────────┬──────────────────────┬───────────┐
│ Priority │ What │ Ticket │ Effort │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 1 │ Wire RefreshCoordinator HTTP calls to operators │ NEWT-628 (remaining) │ 2-3 days │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 2 │ Full protocol E2E test (gateway-driven refresh) │ NEWT-628 (remaining) │ 1-2 days │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 3 │ EpochRegistry Solidity contract │ NEWT-639 │ 1-2 weeks │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 4 │ On-chain aggregation commitments │ NEWT-630 │ 1 week │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 5 │ Privacy-specific slashing conditions │ NEWT-629 │ 1 week │
├──────────┼─────────────────────────────────────────────────┼──────────────────────┼───────────┤
│ 6 │ PrivacySlasher + DleqVerifier contracts │ NEWT-640/641 │ 2 weeks │
└──────────┴─────────────────────────────────────────────────┴──────────────────────┴───────────┘
Items 1-2 complete NEWT-628. Items 3-6 are the remaining Phase 2 tickets. After those, Phase 2 is done and the system has forward
secrecy, on-chain auditability, and economic security for privacy operations.