newton-aggregator 0.4.18

newton prover aggregator utils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
use alloy::primitives::{Address, Bytes, FixedBytes, B256, U256};
use chrono::{DateTime, Utc};
use eigensdk::{crypto_bls::Signature, types::operator::OperatorId};
use newton_core::{
    newton_prover_task_manager::{INewtonPolicy::PolicyConfig as BindingPolicyConfig, NewtonMessage},
    r#newton_prover_task_manager::INewtonProverTaskManager::TaskResponse as BindingTaskResponse,
    TaskId,
};
use serde::{Deserialize, Serialize};

#[cfg(feature = "threshold")]
use curve25519_dalek::{edwards::CompressedEdwardsY, scalar::Scalar};

/// Operator index and compressed Edwards public share bytes for threshold DKG.
pub type PublicShare = (u32, Vec<u8>);

/// Threshold configuration: (min_signers, total_shares).
pub type ThresholdConfig = (u32, u32);

/// Serde-compatible TaskResponse wrapper
/// Note: policyTaskData and policyConfig are now included (operators generate these independently)
#[derive(Clone, Serialize, Deserialize)]
pub struct TaskResponse {
    /// Task ID
    pub task_id: B256,
    /// Policy Client
    pub policy_client: Address,
    /// Policy ID
    pub policy_id: B256,
    /// Policy Address
    pub policy_address: Address,
    /// Intent
    pub intent: NewtonMessage::Intent,
    /// Intent Signature
    pub intent_signature: Bytes,
    /// Evaluation result
    pub evaluation_result: Vec<u8>,
    /// Policy task data - generated by operators, validated by aggregator
    pub policy_task_data: NewtonMessage::PolicyTaskData,
    /// Policy config - fetched by operators from chain
    pub policy_config: BindingPolicyConfig,
    /// timestamp marking the offchain ingestion of the task
    pub initialization_timestamp: U256,
}

impl std::fmt::Debug for TaskResponse {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TaskResponse")
            .field("task_id", &self.task_id)
            .field("policy_client", &self.policy_client)
            .field("policy_id", &self.policy_id)
            .field("policy_address", &self.policy_address)
            .field("evaluation_result", &self.evaluation_result)
            .finish_non_exhaustive()
    }
}

impl From<BindingTaskResponse> for TaskResponse {
    fn from(binding: BindingTaskResponse) -> Self {
        Self {
            task_id: binding.taskId,
            policy_client: binding.policyClient,
            policy_id: binding.policyId,
            policy_address: binding.policyAddress,
            intent: binding.intent,
            intent_signature: binding.intentSignature,
            evaluation_result: binding.evaluationResult.to_vec(),
            policy_task_data: binding.policyTaskData,
            policy_config: binding.policyConfig,
            initialization_timestamp: binding.initializationTimestamp,
        }
    }
}

impl From<TaskResponse> for BindingTaskResponse {
    fn from(task_response: TaskResponse) -> Self {
        Self {
            taskId: task_response.task_id,
            policyClient: task_response.policy_client,
            policyId: task_response.policy_id,
            policyAddress: task_response.policy_address,
            intent: task_response.intent,
            intentSignature: task_response.intent_signature,
            evaluationResult: task_response.evaluation_result.into(),
            policyTaskData: task_response.policy_task_data,
            policyConfig: task_response.policy_config,
            initializationTimestamp: task_response.initialization_timestamp,
        }
    }
}

// use alloy::sol_types::SolCall;
/// Signed Task Response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedTaskResponse {
    /// Task Response
    pub task_response: TaskResponse,
    /// Task ID
    pub task_id: TaskId,
    signature: Signature,
    operator_id: OperatorId,
}

impl SignedTaskResponse {
    /// Create a new [`SignedTaskResponse`]
    pub fn new(
        task_id: TaskId,
        task_response: TaskResponse,
        bls_signature: Signature,
        operator_id: OperatorId,
    ) -> Self {
        Self {
            task_id,
            task_response,
            signature: bls_signature,
            operator_id,
        }
    }

    /// [`Signature`]
    pub fn signature(&self) -> Signature {
        self.signature.clone()
    }

    /// [`OperatorId`]
    pub fn operator_id(&self) -> OperatorId {
        self.operator_id
    }
}

/// Operator error response for task processing failures
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatorErrorResponse {
    /// Task ID
    pub task_id: TaskId,
    /// Operator ID
    pub operator_id: OperatorId,
    /// Error code (JSON-RPC error code)
    pub error_code: i32,
    /// Error message
    pub error_message: String,
    /// Timestamp
    pub timestamp: DateTime<Utc>,
}

// ═══════════════════════════════════════════════════════════════════════════════
// Two-Phase Consensus Protocol Types
// ═══════════════════════════════════════════════════════════════════════════════
//
// The Two-Phase Protocol separates data fetching from policy evaluation/BLS signing
// to ensure all operators sign the SAME consensus digest. This solves the fundamental
// issue where operators independently fetch time-varying data (e.g., oracle prices),
// producing different policyTaskData that cannot be aggregated via BLS.
//
// Flow:
//   Prepare Phase: Operators fetch data → return ConsensusPrepareResponse (just policyTaskData)
//   Consensus: Gateway computes median on policyTaskData → produces ConsensusData
//   Commit Phase: Gateway sends consensus policyTaskData → Operators evaluate + BLS sign
//   Aggregate: All signatures over same TaskResponse → BLS aggregation succeeds
//
// Key insight: BLS signature covers TaskResponse which includes policyTaskData.
// By having all operators evaluate with the SAME consensus policyTaskData, they
// produce identical TaskResponses with identical digests.
// ═══════════════════════════════════════════════════════════════════════════════

/// Prepare phase request - fetch policy data
///
/// Gateway sends this to operators to fetch policyTaskData without evaluation or signing.
/// When `enc_point` is present, operators with threshold key shares also compute
/// a partial decryption and DLEQ proof.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusPrepareRequest {
    /// Task ID for this request
    pub task_id: TaskId,
    /// Policy client address
    pub policy_client: Address,
    /// Intent to evaluate
    pub intent: NewtonMessage::Intent,
    /// Intent signature from intent creator
    pub intent_signature: Bytes,
    /// WASM args for data generation
    pub wasm_args: Bytes,
    /// HPKE encapsulated key (32 bytes, X25519 Montgomery point) for threshold decryption.
    /// When present, operators compute `D_i = s_i * enc` and return a DLEQ proof.
    /// Absent for non-privacy tasks.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enc_point: Option<Vec<u8>>,
    /// HPKE encapsulated keys from inline ephemeral envelopes for threshold partial DH.
    /// Each entry is a raw 32-byte X25519 Montgomery point.
    /// When present, operators compute partial DH + DLEQ proof for each point.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ephemeral_enc_points: Option<Vec<Vec<u8>>>,
    /// Peer enclave ephemeral X25519 public keys for per-peer partial DH encryption.
    /// Each entry is (operator_id, 32-byte X25519 pubkey). Excludes the recipient operator.
    /// Present only in threshold mode (Phase 1b). Operators forward these to their enclave
    /// so partial DH outputs can be encrypted per-peer.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub peer_enclave_pubkeys: Option<Vec<(OperatorId, Vec<u8>)>>,
}

/// Chain context overrides for Prepare phase in multichain mode.
///
/// In multi-chain mode, operators are configured for the source chain but may need
/// to fetch policy data from destination chains. These overrides tell the operator
/// which chain's RPC and contracts to use for EIP712 verification, WASM execution,
/// and policy data fetching.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparePhaseChainContext {
    /// RPC URL for the target chain where policy contracts are deployed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chain_rpc_url: Option<String>,
    /// Target chain ID for EIP712 domain and signing mode selection.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_chain_id: Option<u64>,
    /// Target chain's TaskManager address for EIP712 domain verification.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_task_manager_addr: Option<Address>,
}

/// Wire-format request for `newt_fetchPolicyData` RPC method.
///
/// Replaces the previous positional params array `[Task, sig, ctx?, enc?, ephemeral?]`
/// with a named-field struct for type safety and extensibility.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreparePhaseFetchRequest {
    /// The on-chain Task struct
    pub task: newton_core::newton_prover_task_manager::INewtonProverTaskManager::Task,
    /// EIP712 signature over the task (65 bytes: r + s + v)
    pub signature: Bytes,
    /// Chain context overrides for multichain mode
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chain_context: Option<PreparePhaseChainContext>,
    /// HPKE encapsulated key (32 bytes) for persistent privacy threshold decryption
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enc_point: Option<Vec<u8>>,
    /// HPKE encapsulated keys from inline ephemeral envelopes for threshold partial DH
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ephemeral_enc_points: Option<Vec<Vec<u8>>>,
    /// Identity registry address for resolving identity data during Prepare phase (threshold mode).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_registry: Option<Address>,
    /// Confidential data registry address for resolving confidential data during Prepare phase (threshold mode).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidential_data_registry: Option<Address>,
    /// Policy client address for registry queries during Prepare phase.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_client: Option<Address>,
    /// Intent for signer recovery during Prepare phase identity resolution.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub intent: Option<newton_core::newton_prover_task_manager::NewtonMessage::Intent>,
    /// Intent signature for signer recovery during Prepare phase identity resolution.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub intent_signature: Option<Bytes>,
    /// Peer enclave ephemeral X25519 public keys for per-peer partial DH encryption.
    /// Each entry is (operator_id, 32-byte X25519 pubkey). Excludes the recipient operator.
    /// Present only in threshold mode (Phase 1b).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub peer_enclave_pubkeys: Option<Vec<(OperatorId, Vec<u8>)>>,
}

/// Partial decryption data returned by an operator during Prepare phase.
///
/// When the gateway sends an `enc_point` in the Prepare request, operators with
/// threshold key shares compute `D_i = s_i * enc_edwards` and generate a DLEQ proof
/// demonstrating `log_G(pk_i) == log_{enc}(D_i)`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartialDecryptionData {
    /// Partial DH output: `D_i = s_i * enc_edwards` (compressed Edwards, 32 bytes)
    pub partial_point: Vec<u8>,
    /// Operator's public key share: `pk_i = s_i * G` (compressed Edwards, 32 bytes)
    pub public_share: Vec<u8>,
    /// DLEQ proof challenge scalar (32 bytes)
    pub dleq_challenge: Vec<u8>,
    /// DLEQ proof response scalar (32 bytes)
    pub dleq_response: Vec<u8>,
    /// Operator index in the threshold scheme (1-based, matches Feldman VSS evaluation point)
    pub operator_index: u32,
}

#[cfg(feature = "threshold")]
impl From<newton_core::dkg::PartialDecryption> for PartialDecryptionData {
    fn from(pd: newton_core::dkg::PartialDecryption) -> Self {
        Self {
            partial_point: pd.partial.compress().as_bytes().to_vec(),
            public_share: Vec::new(), // not needed on wire — gateway has public_shares map
            dleq_challenge: pd.proof.challenge.as_bytes().to_vec(),
            dleq_response: pd.proof.response.as_bytes().to_vec(),
            operator_index: pd.index,
        }
    }
}

#[cfg(feature = "threshold")]
impl TryFrom<&PartialDecryptionData> for newton_core::dkg::PartialDecryption {
    type Error = String;

    fn try_from(wire: &PartialDecryptionData) -> Result<Self, Self::Error> {
        let partial_bytes: [u8; 32] = wire
            .partial_point
            .as_slice()
            .try_into()
            .map_err(|_| format!("partial_point must be 32 bytes, got {}", wire.partial_point.len()))?;
        let partial = CompressedEdwardsY(partial_bytes)
            .decompress()
            .ok_or("invalid partial_point: not on Edwards curve")?;

        let challenge_bytes: [u8; 32] = wire
            .dleq_challenge
            .as_slice()
            .try_into()
            .map_err(|_| format!("dleq_challenge must be 32 bytes, got {}", wire.dleq_challenge.len()))?;
        let challenge = Scalar::from_canonical_bytes(challenge_bytes)
            .into_option()
            .ok_or("invalid dleq_challenge scalar")?;

        let response_bytes: [u8; 32] = wire
            .dleq_response
            .as_slice()
            .try_into()
            .map_err(|_| format!("dleq_response must be 32 bytes, got {}", wire.dleq_response.len()))?;
        let response = Scalar::from_canonical_bytes(response_bytes)
            .into_option()
            .ok_or("invalid dleq_response scalar")?;

        Ok(newton_core::dkg::PartialDecryption {
            index: wire.operator_index,
            partial,
            proof: newton_core::dkg::DleqProof { challenge, response },
        })
    }
}

/// Prepare phase response - unsigned policy data only
///
/// Operators fetch policyTaskData using WASM execution and oracle queries.
/// NO policy evaluation, NO BLS signing - just the raw data.
/// Gateway collects these, computes median consensus, then sends to Commit phase.
///
/// When the request included an `enc_point`, operators also return a `partial_decryption`
/// with their partial DH output and DLEQ proof for threshold decryption.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusPrepareResponse {
    /// Task ID this response is for
    pub task_id: TaskId,
    /// Operator ID that generated this response
    pub operator_id: OperatorId,
    /// Policy task data fetched by operator (UNSIGNED)
    pub policy_task_data: NewtonMessage::PolicyTaskData,
    /// Hash of the policy task data (for Evaluate phase tolerance verification)
    pub data_hash: FixedBytes<32>,
    /// Timestamp when the data was fetched
    pub timestamp: DateTime<Utc>,
    /// Per-peer encrypted partial DH blobs (Phase 1b TEE threshold mode).
    /// Each blob is HPKE-encrypted to the recipient peer's enclave ephemeral pubkey.
    /// The gateway relays these opaque blobs without being able to read them.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encrypted_partials: Option<Vec<newton_core::crypto::EncryptedPartialDH>>,
    /// Partial decryption for threshold HPKE (legacy, pre-TEE path).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub partial_decryption: Option<PartialDecryptionData>,
    /// Partial decryptions for inline ephemeral envelopes (one per ephemeral_enc_point).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ephemeral_partial_decryptions: Option<Vec<PartialDecryptionData>>,
    /// Partial decryptions for identity envelopes (one per resolved identity domain).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_partial_decryptions: Option<Vec<PartialDecryptionData>>,
    /// Partial decryptions for confidential envelopes (one per resolved confidential domain).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidential_partial_decryptions: Option<Vec<PartialDecryptionData>>,
    /// Data ref IDs resolved for identity domains (for cross-operator consistency verification).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_data_ref_ids: Option<Vec<String>>,
    /// Data ref IDs resolved for confidential domains (for cross-operator consistency verification).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidential_data_ref_ids: Option<Vec<String>>,
}

/// Commit phase request - evaluate and sign with consensus data
///
/// After Prepare phase responses are collected and consensus policyTaskData is computed,
/// the gateway sends this request to operators to:
/// 1. Evaluate policy with consensus policyTaskData
/// 2. Build TaskResponse with consensus policyTaskData
/// 3. BLS sign the TaskResponse
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusCommitRequest {
    /// Task ID to evaluate and sign
    pub task_id: TaskId,
    /// Consensus policyTaskData from Prepare phase (all operators use THIS)
    pub consensus_policy_task_data: NewtonMessage::PolicyTaskData,
    /// Intent to evaluate
    pub intent: NewtonMessage::Intent,
    /// Intent signature from intent creator
    pub intent_signature: Bytes,
    /// Policy client address
    pub policy_client: Address,
    /// Block number for BLS quorum APK lookups
    pub task_created_block: u32,
    /// Quorum numbers for task signing (determines operator set ID on destination chains)
    pub quorum_numbers: Bytes,
    /// Quorum threshold percentage required for BLS aggregation (0-100)
    pub quorum_threshold_percentage: u32,
    /// timestamp marking the offchain ingestion of the task
    pub initialization_timestamp: u64,
    /// WASM args used for policyTaskData generation (for Task struct construction)
    pub wasm_args: Bytes,
    /// RPC URL for the target chain where policy contracts are deployed.
    /// In multi-chain mode, this may differ from the operator's default RPC URL.
    /// When `None`, the operator uses its configured default RPC URL.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chain_rpc_url: Option<String>,
    /// Target chain ID for BLS signing mode selection.
    /// In multi-chain mode, this tells the operator whether the target chain is a
    /// destination chain (requiring EIP712 certificate signing) or source chain (plain hash).
    /// When `None`, the operator uses its configured default chain ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_chain_id: Option<u64>,
    /// Target chain's TaskManager address for destination chain certificate resolution.
    /// Required when `target_chain_id` is a destination chain so the operator can
    /// resolve the certificate verifier and reference timestamp for EIP712 signing.
    /// When `None`, the operator uses its configured default TaskManager address.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target_task_manager_addr: Option<Address>,
    /// Raw HPKE SecureEnvelopes for operator-side decryption.
    /// Gateway forwards validated but undecrypted envelopes; operators decrypt locally
    /// using their own `hpke_sk` or by combining threshold partials.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ephemeral_envelopes: Option<Vec<newton_core::crypto::SecureEnvelope>>,
    /// Threshold partial decryptions collected from all operators during Prepare phase.
    /// Gateway forwards these without combining — each operator combines + decrypts locally.
    /// Outer Vec = per envelope (matching ephemeral_envelopes indices),
    /// inner Vec = per operator partial DH output.
    /// Present only in threshold mode; None when threshold is not enabled.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub threshold_partial_decryptions: Option<Vec<Vec<PartialDecryptionData>>>,
    /// Threshold public shares for operators to reconstruct ThresholdDecryptionContext.
    /// Required in threshold mode for operators to combine partial decryptions locally.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub threshold_public_shares: Option<Vec<PublicShare>>,
    /// Threshold configuration (threshold, total_shares) needed alongside public_shares.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub threshold_config: Option<ThresholdConfig>,
    /// Optional IPFS CID of a TLSNotary presentation proof for zkTLS verification.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub proof_cid: Option<String>,
    /// Data ref IDs for identity domains (operators fetch encrypted envelopes from DB during Commit).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_data_ref_ids: Option<Vec<String>>,
    /// Threshold partial decryptions for identity envelopes.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identity_threshold_partials: Option<Vec<Vec<PartialDecryptionData>>>,
    /// Data ref IDs for confidential domains (operators fetch encrypted envelopes from DB during Commit).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidential_data_ref_ids: Option<Vec<String>>,
    /// Threshold partial decryptions for confidential envelopes.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub confidential_threshold_partials: Option<Vec<Vec<PartialDecryptionData>>>,
    /// Per-peer encrypted partial DH blobs collected from all operators during Prepare.
    /// Each operator's enclave encrypted its partials to each peer. The gateway collects
    /// all blobs and forwards the full set to each operator during Commit. Each enclave
    /// filters for blobs addressed to itself and decrypts with its ephemeral private key.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub encrypted_peer_partials: Option<Vec<newton_core::crypto::EncryptedPartialDH>>,
    /// Number of ephemeral envelopes whose partials are in the encrypted blobs.
    /// The enclave uses this to split decrypted partials: `[0..eph_count]` are ephemeral.
    #[serde(default)]
    pub encrypted_partials_ephemeral_count: usize,
    /// Number of identity envelopes: `[eph_count..eph_count+id_count]` are identity.
    #[serde(default)]
    pub encrypted_partials_identity_count: usize,
    /// Number of confidential envelopes: remainder after identity are confidential.
    #[serde(default)]
    pub encrypted_partials_confidential_count: usize,
    /// EIP-712 signature from an authorized task generator over the canonical
    /// `Task` struct (same domain/scheme as Prepare phase). Octane #8: prior to
    /// this field the Commit RPC was unauthenticated, letting a malicious caller
    /// collect operator BLS signatures over attacker-chosen TaskResponses for
    /// `validateAttestationDirect`. The operator reconstructs the `Task` from
    /// `(task_id, task_created_block, policy_client, quorum_numbers,
    /// quorum_threshold_percentage, intent, intent_signature, wasm_args,
    /// initialization_timestamp)`, then verifies this signature against the
    /// `OperatorRegistry` task-generator allowlist before doing any work.
    /// `Option` only for cross-version JSON compatibility — the operator
    /// rejects requests where this field is absent or empty.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub task_signature: Option<Bytes>,
}

/// Commit phase response - evaluation result + BLS signature
///
/// Operators evaluate policy with consensus policyTaskData (same input → same output),
/// build TaskResponse (includes consensus policyTaskData), and BLS sign it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsensusCommitResponse {
    /// Task ID this response is for
    pub task_id: TaskId,
    /// Policy evaluation result
    pub evaluation_result: Vec<u8>,
    /// Full TaskResponse with consensus policyTaskData (ready for on-chain submission)
    pub task_response: TaskResponse,
    /// BLS signature over the consensus digest
    pub signature: Signature,
    /// Operator ID that produced this response
    pub operator_id: OperatorId,
    /// Per-task Nitro attestation document (~3KB CBOR) for privacy tasks.
    /// Empty when enclave does not produce attestation (pre-NSM integration).
    #[serde(default)]
    pub attestation_data: alloy::primitives::Bytes,
}

/// Consensus data computed from Prepare phase unsigned responses
///
/// Contains the normalized policyTaskData, its hash, and any adjustments made
/// during median normalization.
#[derive(Debug, Clone)]
pub struct ConsensusData {
    /// Normalized policyTaskData (median values applied, attestations cleared)
    pub policy_task_data: NewtonMessage::PolicyTaskData,
    /// Hash of the consensus policyTaskData
    pub data_hash: FixedBytes<32>,
    /// Field adjustments made during normalization (for debugging/logging)
    pub field_adjustments: Vec<FieldAdjustment>,
}

/// Records a field adjustment made during median normalization
#[derive(Debug, Clone)]
pub struct FieldAdjustment {
    /// JSON path to the adjusted field (e.g., "policyData[0].data.price")
    pub field_path: String,
    /// Original values from each operator
    pub original_values: Vec<f64>,
    /// Median value used in consensus
    pub median_value: f64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn consensus_commit_request_proof_cid_is_backward_compatible() {
        let json_without_proof = json!({
            "task_id": "0x0000000000000000000000000000000000000000000000000000000000000001",
            "consensus_policy_task_data": {
                "policyId": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "policyAddress": "0x0000000000000000000000000000000000000000",
                "policy": "0x",
                "policyData": []
            },
            "policy_config": {
                "policyParams": "0x",
                "expireAfter": 100
            },
            "intent": {
                "from": "0x0000000000000000000000000000000000000000",
                "to": "0x0000000000000000000000000000000000000000",
                "value": "0x0",
                "data": "0x",
                "chainId": "0x1",
                "functionSignature": "0x"
            },
            "intent_signature": "0x",
            "policy_client": "0x0000000000000000000000000000000000000000",
            "task_created_block": 100,
            "quorum_numbers": "0x00",
            "quorum_threshold_percentage": 40,
            "initialization_timestamp": 123,
            "wasm_args": "0x"
        });

        let request: ConsensusCommitRequest = serde_json::from_value(json_without_proof).unwrap();
        assert!(request.proof_cid.is_none());

        let json_with_proof = json!({
            "task_id": "0x0000000000000000000000000000000000000000000000000000000000000001",
            "consensus_policy_task_data": {
                "policyId": "0x0000000000000000000000000000000000000000000000000000000000000000",
                "policyAddress": "0x0000000000000000000000000000000000000000",
                "policy": "0x",
                "policyData": []
            },
            "policy_config": {
                "policyParams": "0x",
                "expireAfter": 100
            },
            "intent": {
                "from": "0x0000000000000000000000000000000000000000",
                "to": "0x0000000000000000000000000000000000000000",
                "value": "0x0",
                "data": "0x",
                "chainId": "0x1",
                "functionSignature": "0x"
            },
            "intent_signature": "0x",
            "policy_client": "0x0000000000000000000000000000000000000000",
            "task_created_block": 100,
            "quorum_numbers": "0x00",
            "quorum_threshold_percentage": 40,
            "initialization_timestamp": 123,
            "wasm_args": "0x",
            "proof_cid": "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
        });

        let request_with_proof: ConsensusCommitRequest = serde_json::from_value(json_with_proof).unwrap();
        assert_eq!(
            request_with_proof.proof_cid.as_deref(),
            Some("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")
        );
    }
}