newton-aggregator 0.4.12

newton prover aggregator utils
Documentation
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
//! Per-tick canonical operator-table snapshot for the state-commit aggregator.
//!
//! The on-chain `BN254CertificateVerifier` requires every certificate to carry:
//!
//! 1. A `referenceTimestamp` matching a previously-confirmed operator-table root
//!    (per `OperatorTableUpdater.confirmGlobalTableRoot`).
//! 2. The full operator-set G2 APK as `cert.apk` — minus non-signers via the
//!    G1-form `nonSignerWitnesses`.
//! 3. Merkle witnesses for each non-signer, proving inclusion against the
//!    confirmed `operatorInfoTreeRoot`.
//!
//! All three derive from the same source: the on-chain `BN254TableCalculator`'s
//! canonical operator list at the current `referenceTimestamp`. This module
//! defines the snapshot data shape ([`OperatorTableSnapshot`]) and the provider
//! trait ([`OperatorTableProvider`]) that fetches it. The actual cache + reactive
//! refresh on `NewGlobalTableRoot` events is wired separately (next phase); for
//! now the orchestrator can fetch a fresh snapshot per-tick.
//!
//! ## Alignment invariant
//!
//! The snapshot's `operators` field must be in the SAME order the on-chain
//! calculator used to build `operatorInfoTreeRoot`. Newton's
//! `BN254TableCalculator.getOperatorInfos` returns operators in the order they
//! appear in `_operatorSetData[opSetHash].operators[]`, which is also the order
//! used by `_calculateOperatorTable` to construct the merkle tree. The position
//! of an operator in this array IS its `operatorIndex` in the tree — the
//! aggregator's witness construction relies on this exact alignment.
//!
//! ## Minimum viable scope (no caching yet)
//!
//! The first cut is per-tick fetch (one RPC roundtrip per 120s commit). The
//! cost is negligible relative to the 120s loop budget; an event-driven cache
//! refresh on `NewGlobalTableRoot` is a follow-up optimization. The trait
//! abstracts the data source so caching can be layered without touching the
//! aggregator.
//!
//! See `crates/aggregator/src/state_commit/operator_table_witness.rs` for the
//! Merkle witness builder that consumes this snapshot.

use std::sync::Arc;

use alloy::primitives::{Address, B256};
use async_trait::async_trait;
use eigensdk::common::SdkProvider;
use newton_prover_core::{
    bn254_certificate_verifier::{
        IOperatorTableCalculatorTypes::BN254OperatorInfo,
        ViewBN254CertificateVerifier::{OperatorSet as VerifierOperatorSet, ViewBN254CertificateVerifierInstance},
        BN254::G1Point,
    },
    bn254_table_calculator::BN254TableCalculator::{
        BN254TableCalculatorInstance, OperatorSet as CalculatorOperatorSet,
    },
};

/// Convert a calculator-binding `BN254OperatorInfo` to the verifier-binding form.
///
/// alloy's `sol!` macro generates a distinct Rust type per binding even when
/// the underlying Solidity struct is identical, so reading from the calculator
/// and feeding to the witness builder (which lives in verifier-binding type
/// space) requires a one-line field copy. The two types share an identical
/// memory layout, but Rust's type system enforces the boundary — converting
/// explicitly keeps the conversion site greppable.
fn calc_op_info_to_verifier(
    calc_info: newton_prover_core::bn254_table_calculator::IOperatorTableCalculatorTypes::BN254OperatorInfo,
) -> BN254OperatorInfo {
    BN254OperatorInfo {
        pubkey: G1Point {
            X: calc_info.pubkey.X,
            Y: calc_info.pubkey.Y,
        },
        weights: calc_info.weights,
    }
}

/// Convert a calculator-binding `G1Point` to the verifier-binding form.
fn calc_g1_to_verifier(calc: newton_prover_core::bn254_table_calculator::BN254::G1Point) -> G1Point {
    G1Point { X: calc.X, Y: calc.Y }
}

/// Snapshot of the on-chain operator-table at a confirmed reference timestamp.
///
/// All fields derive from a single coherent on-chain view: the `operators`
/// array, the `aggregate_pubkey_g1`, and the `operator_info_tree_root` were
/// built from the same input by the on-chain calculator. Mixing them across
/// timestamps will silently fail the on-chain verifier with `VerificationFailed`.
#[derive(Debug, Clone)]
pub struct OperatorTableSnapshot {
    /// Reference timestamp confirmed on-chain via `confirmGlobalTableRoot`.
    /// Must equal the `cert.referenceTimestamp` field for verification to pass.
    pub reference_timestamp: u32,
    /// Canonical ordered operator list. Position in this `Vec` IS the
    /// operator's index in the on-chain Merkle tree
    /// (`operator_info_tree_root`).
    pub operators: Vec<BN254OperatorInfo>,
    /// Aggregate G1 public key over all operators in `operators`. Stored
    /// on-chain as `_operatorSetInfos[key][refTimestamp].aggregatePubkey` —
    /// the verifier subtracts non-signer G1 pubkeys from this to recover
    /// `signers_apk_g1`.
    pub aggregate_pubkey_g1: G1Point,
    /// Merkle root over `operators` (with `LeafCalculatorMixin.OPERATOR_INFO_LEAF_SALT`
    /// salt + `Merkle.merkleizeKeccak` zero-padding). Witnesses must verify
    /// against this root.
    pub operator_info_tree_root: B256,
}

impl OperatorTableSnapshot {
    /// Returns the tree index for an operator identified by its G1 pubkey, or
    /// `None` if the pubkey is not in the canonical list.
    ///
    /// The aggregator uses this to map operators (known by `OperatorId`/G1
    /// pubkey via [`crate::state_commit::aggregator::OperatorRecord`]) to
    /// their Merkle index for witness construction.
    pub fn index_by_g1_pubkey(&self, pubkey: &G1Point) -> Option<u32> {
        self.operators
            .iter()
            .position(|op| op.pubkey == *pubkey)
            .map(|i| i as u32)
    }
}

/// Errors produced by snapshot fetching.
#[derive(Debug, thiserror::Error)]
pub enum OperatorTableError {
    /// On-chain RPC call failed (transport, timeout, contract revert).
    /// Transient: the next tick retries against the same calculator.
    #[error("on-chain query failed: {0}")]
    Rpc(String),

    /// The cert verifier returned `referenceTimestamp == 0`, meaning no
    /// operator-table root has been confirmed yet on this chain. Until the
    /// transporter pushes a confirmed root, signed-read responses cannot
    /// produce `status: confirmed` and state-commits cannot anchor.
    #[error("no operator-table root has been confirmed on-chain yet")]
    NoConfirmedRoot,

    /// The calculator returned an empty operator set or a tree root of zero —
    /// this is the on-chain `empty_set` flavor and surfaces here so the
    /// orchestrator can classify it consistently.
    #[error("operator set is empty: {0}")]
    EmptySet(String),
}

/// Provider trait for fetching the canonical operator-table snapshot.
///
/// Production impl is [`HttpOperatorTableProvider`]; orchestrator unit tests
/// use a hand-rolled fake. The trait is `Send + Sync + 'static` so the
/// orchestrator can hold it behind `Arc<dyn OperatorTableProvider>` across
/// `tokio::spawn` boundaries.
#[async_trait]
pub trait OperatorTableProvider: Send + Sync + 'static {
    /// Fetch a fresh [`OperatorTableSnapshot`] for the given operator set.
    ///
    /// Implementations MUST return a coherent view: all four fields must be
    /// read against the same `referenceTimestamp` so the snapshot can produce
    /// witnesses that verify against the on-chain root.
    async fn fetch_snapshot(
        &self,
        operator_set: VerifierOperatorSet,
    ) -> Result<OperatorTableSnapshot, OperatorTableError>;
}

/// Production HTTP implementation of [`OperatorTableProvider`].
///
/// Reads from on-chain contracts on the **destination chain** — the chain
/// whose `BN254CertificateVerifier` will consume the cert this snapshot
/// gets aggregated into. All three reads MUST target the same chain: the
/// `BN254TableCalculator` and `ViewBN254CertificateVerifier` are both
/// destination-chain artifacts (the source-chain transporter writes to
/// `ECDSAOperatorTableUpdater` on the destination chain, which then
/// confirms a global table root that the dest-chain verifier reads).
///
/// Common bug pattern (caught by wesl-ee on PR #618 review id 3201031404):
/// passing a SOURCE-chain `table_calculator` address with a DEST-chain RPC
/// URL produces an `eth_call` against an address that either has no
/// contract at all (revert) or — worse — a different contract on the dest
/// chain whose calldata happens to ABI-decode (silent wrong answer).
///
/// Reads:
/// - `BN254TableCalculator.getOperatorInfos` — canonical operator list
/// - `BN254TableCalculator.getOperatorSetInfo` — aggregate G1 APK + tree root
/// - `ViewBN254CertificateVerifier.latestReferenceTimestamp` — confirmed timestamp
///
/// `latestReferenceTimestamp` is read first (pins the confirmed snapshot
/// identity); the two calculator reads then run in parallel via
/// `tokio::try_join!`. Caching with reactive refresh on the
/// `NewGlobalTableRoot` event is a follow-up.
#[derive(Debug)]
pub struct HttpOperatorTableProvider {
    /// Address of the destination chain's `BN254TableCalculator`.
    table_calculator: Address,
    /// Address of the destination chain's `ViewBN254CertificateVerifier`.
    cert_verifier: Address,
    /// Cached HTTP provider — reuses connection pool across snapshot fetches.
    /// Per `lessons.md` "Cached provider handles must document RPC-URL
    /// immutability at the field site": the URL is fixed at construction.
    /// If RPC URL rotation is ever added, the provider must be rebuilt.
    /// `dest_rpc_url` MUST be the destination chain's RPC — same chain where
    /// `table_calculator` and `cert_verifier` are deployed.
    provider: SdkProvider,
}

impl HttpOperatorTableProvider {
    /// Construct from already-resolved destination-chain contract addresses
    /// and RPC URL.
    ///
    /// Naming the parameter `dest_rpc_url` (not just `rpc_url`) makes the
    /// chain context explicit at every call site so the
    /// "source-calculator-against-dest-RPC" mistake (PR #618 review id
    /// 3201031404) becomes visually obvious.
    pub fn new(table_calculator: Address, cert_verifier: Address, dest_rpc_url: &str) -> Self {
        Self {
            table_calculator,
            cert_verifier,
            provider: eigensdk::common::get_provider(dest_rpc_url),
        }
    }
}

#[async_trait]
impl OperatorTableProvider for HttpOperatorTableProvider {
    async fn fetch_snapshot(
        &self,
        operator_set: VerifierOperatorSet,
    ) -> Result<OperatorTableSnapshot, OperatorTableError> {
        let calculator = BN254TableCalculatorInstance::new(self.table_calculator, &self.provider);
        let verifier = ViewBN254CertificateVerifierInstance::new(self.cert_verifier, &self.provider);

        // Field type for the calculator is in calculator types; field type for
        // the verifier is in verifier types. The two share the same Solidity
        // shape (avs + id) but alloy gives them distinct Rust types, so we
        // convert at the boundary.
        let calculator_op_set = CalculatorOperatorSet {
            avs: operator_set.avs,
            id: operator_set.id,
        };

        // 1. Read `latestReferenceTimestamp` FIRST — this is the verifier's
        //    confirmed snapshot identity. Reading it before the calculator
        //    queries minimises the window in which an on-chain
        //    `NewGlobalTableRoot` could land between this read and the
        //    subsequent calculator reads, leaving the orchestrator with a
        //    stale verifier timestamp paired against a fresher calculator
        //    snapshot — the cert would then build against an inconsistent
        //    `(operator_set, reference_timestamp)` pair and surface as
        //    `ReferenceTimestampDoesNotExist` at certificate verification.
        //    Worst-case race window collapses to "calculator state advances
        //    after the timestamp read" rather than the prior "verifier
        //    timestamp advances after the calculator reads".
        let reference_timestamp = verifier
            .latestReferenceTimestamp(operator_set.clone())
            .call()
            .await
            .map_err(|e| OperatorTableError::Rpc(format!("latestReferenceTimestamp: {e}")))?;
        if reference_timestamp == 0 {
            return Err(OperatorTableError::NoConfirmedRoot);
        }

        // 2. Fetch the canonical operator list AND aggregate pubkey + tree
        //    root from the calculator in parallel — they're independent
        //    reads with no data dependency between them. `tokio::try_join!`
        //    collapses two sequential round-trips to one round-trip-of-
        //    latency at the cost of two parallel connections (fine for the
        //    hot path; reqwest's pool reuses TCP). Either error short-
        //    circuits the join.
        //
        // The `.call()` builders need to live in `let`-bindings so the
        // futures borrow named values, not temporaries dropped at the end
        // of the macro expansion.
        let infos_call = calculator.getOperatorInfos(calculator_op_set.clone());
        let set_info_call = calculator.getOperatorSetInfo(calculator_op_set);
        let (calc_operators_res, set_info_res) = tokio::try_join!(infos_call.call(), set_info_call.call(),)
            .map_err(|e| OperatorTableError::Rpc(format!("calculator parallel read: {e}")))?;

        if calc_operators_res.is_empty() {
            return Err(OperatorTableError::EmptySet(format!(
                "BN254TableCalculator.getOperatorInfos returned 0 operators for set {:?}",
                operator_set
            )));
        }
        let operators: Vec<BN254OperatorInfo> = calc_operators_res.into_iter().map(calc_op_info_to_verifier).collect();
        let set_info = set_info_res;
        if set_info.operatorInfoTreeRoot.is_zero() {
            return Err(OperatorTableError::EmptySet(format!(
                "BN254TableCalculator.getOperatorSetInfo returned zero tree root for set {:?}",
                operator_set
            )));
        }

        Ok(OperatorTableSnapshot {
            reference_timestamp,
            operators,
            aggregate_pubkey_g1: calc_g1_to_verifier(set_info.aggregatePubkey),
            operator_info_tree_root: set_info.operatorInfoTreeRoot,
        })
    }
}

/// Hand-rolled fake for orchestrator unit tests and dev-stub binary mode.
///
/// Returns a static snapshot constructed at build time. Tests can swap the
/// snapshot to exercise specific tree shapes (full quorum, partial attendance,
/// empty set, etc.) without spinning up an anvil fork.
#[cfg(any(test, feature = "dev-stub"))]
pub mod fake {
    use super::*;
    use alloy::primitives::U256;

    /// Static-snapshot fake implementing [`OperatorTableProvider`].
    #[derive(Debug)]
    pub struct FakeOperatorTableProvider {
        snapshot: OperatorTableSnapshot,
    }

    impl FakeOperatorTableProvider {
        /// Construct from a pre-built snapshot.
        pub fn new(snapshot: OperatorTableSnapshot) -> Arc<Self> {
            Arc::new(Self { snapshot })
        }

        /// Convenience: build a snapshot from raw operator data for tests
        /// that don't care about the `(aggregate_pubkey, tree_root)` fields
        /// being mathematically consistent (those are validated end-to-end
        /// by the integration tests against real `BN254CertificateVerifier`).
        pub fn from_operators(reference_timestamp: u32, operators: Vec<BN254OperatorInfo>) -> Arc<Self> {
            Arc::new(Self {
                snapshot: OperatorTableSnapshot {
                    reference_timestamp,
                    operators,
                    aggregate_pubkey_g1: G1Point {
                        X: U256::ZERO,
                        Y: U256::ZERO,
                    },
                    operator_info_tree_root: B256::ZERO,
                },
            })
        }
    }

    #[async_trait]
    impl OperatorTableProvider for FakeOperatorTableProvider {
        async fn fetch_snapshot(
            &self,
            _operator_set: VerifierOperatorSet,
        ) -> Result<OperatorTableSnapshot, OperatorTableError> {
            Ok(self.snapshot.clone())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy::primitives::U256;

    fn op_info(x: u8, y: u8) -> BN254OperatorInfo {
        BN254OperatorInfo {
            pubkey: G1Point {
                X: U256::from(x),
                Y: U256::from(y),
            },
            weights: vec![U256::from(100)],
        }
    }

    #[test]
    fn index_by_g1_pubkey_finds_canonical_position() {
        let snap = OperatorTableSnapshot {
            reference_timestamp: 42,
            operators: vec![op_info(1, 2), op_info(3, 4), op_info(5, 6)],
            aggregate_pubkey_g1: G1Point {
                X: U256::ZERO,
                Y: U256::ZERO,
            },
            operator_info_tree_root: B256::ZERO,
        };

        assert_eq!(
            snap.index_by_g1_pubkey(&G1Point {
                X: U256::from(3),
                Y: U256::from(4)
            }),
            Some(1)
        );
        assert_eq!(
            snap.index_by_g1_pubkey(&G1Point {
                X: U256::from(99),
                Y: U256::from(99)
            }),
            None
        );
    }

    #[tokio::test]
    async fn fake_provider_returns_constructed_snapshot() {
        let snapshot = OperatorTableSnapshot {
            reference_timestamp: 12345,
            operators: vec![op_info(1, 2)],
            aggregate_pubkey_g1: G1Point {
                X: U256::from(1),
                Y: U256::from(2),
            },
            operator_info_tree_root: B256::repeat_byte(0xAB),
        };
        let provider = fake::FakeOperatorTableProvider::new(snapshot.clone());
        let op_set = VerifierOperatorSet {
            avs: Address::repeat_byte(0x11),
            id: 0,
        };

        let fetched = provider.fetch_snapshot(op_set).await.expect("fetch");
        assert_eq!(fetched.reference_timestamp, 12345);
        assert_eq!(fetched.operators.len(), 1);
        assert_eq!(fetched.operator_info_tree_root, B256::repeat_byte(0xAB));
    }
}