Skip to main content

blueprint_tangle_extra/
aggregation.rs

1//! BLS Signature Aggregation for Tangle Jobs
2//!
3//! This module provides types and utilities for submitting aggregated BLS signatures
4//! to the Tangle contracts for jobs that require signature aggregation.
5//!
6//! ## Overview
7//!
8//! When a blueprint's service manager enables aggregation for a job, operators must
9//! collectively sign the job result using BLS signatures. The signatures are then
10//! aggregated and submitted to the contract for verification.
11//!
12//! ## Usage
13//!
14//! ```rust,ignore
15//! use blueprint_tangle_extra::aggregation::{AggregatedResult, SignerBitmap};
16//!
17//! // Create an aggregated result from signatures
18//! let result = AggregatedResult {
19//!     service_id: 1,
20//!     call_id: 42,
21//!     output: output_bytes,
22//!     signer_bitmap: SignerBitmap::from_indices(&[0, 1, 3]),
23//!     signature: aggregated_sig,
24//!     pubkey: aggregated_pubkey,
25//! };
26//!
27//! // Submit to contract
28//! result.submit(&client).await?;
29//! ```
30
31use alloy_primitives::{Bytes, U256};
32use blueprint_client_tangle::TangleClient;
33use blueprint_std::format;
34use blueprint_std::string::String;
35use blueprint_std::sync::Arc;
36use thiserror::Error;
37
38/// Error types for aggregation operations
39#[derive(Debug, Error)]
40pub enum AggregationError {
41    /// Client error
42    #[error("Client error: {0}")]
43    Client(String),
44    /// Not enough signers
45    #[error("Threshold not met: got {0} signers, need {1}")]
46    ThresholdNotMet(usize, usize),
47    /// Invalid signature
48    #[error("Invalid BLS signature")]
49    InvalidSignature,
50    /// Contract call failed
51    #[error("Contract call failed: {0}")]
52    ContractError(String),
53    /// Missing operator key
54    #[error("Missing BLS key for operator at index {0}")]
55    MissingOperatorKey(usize),
56}
57
58/// Bitmap indicating which operators signed
59///
60/// Bit i is set if operator i (in service operator list order) signed.
61#[derive(Debug, Clone, Default)]
62pub struct SignerBitmap(pub U256);
63
64impl SignerBitmap {
65    /// Create a new empty bitmap
66    pub fn new() -> Self {
67        Self(U256::ZERO)
68    }
69
70    /// Create a bitmap from a list of signer indices
71    pub fn from_indices(indices: &[usize]) -> Self {
72        let mut bitmap = U256::ZERO;
73        for &idx in indices {
74            bitmap |= U256::from(1u64) << idx;
75        }
76        Self(bitmap)
77    }
78
79    /// Check if operator at index is a signer
80    pub fn is_signer(&self, index: usize) -> bool {
81        (self.0 >> index) & U256::from(1u64) == U256::from(1u64)
82    }
83
84    /// Add a signer at the given index
85    pub fn add_signer(&mut self, index: usize) {
86        self.0 |= U256::from(1u64) << index;
87    }
88
89    /// Remove a signer at the given index
90    pub fn remove_signer(&mut self, index: usize) {
91        self.0 &= !(U256::from(1u64) << index);
92    }
93
94    /// Count the number of signers
95    pub fn count_signers(&self) -> usize {
96        let mut count = 0;
97        let mut bitmap = self.0;
98        while bitmap > U256::ZERO {
99            if bitmap & U256::from(1u64) == U256::from(1u64) {
100                count += 1;
101            }
102            bitmap >>= 1;
103        }
104        count
105    }
106
107    /// Get the raw U256 value
108    pub fn as_u256(&self) -> U256 {
109        self.0
110    }
111}
112
113/// BLS G1 point (signature) in contract format
114///
115/// Represented as two 256-bit integers [x, y]
116#[derive(Debug, Clone)]
117pub struct G1Point {
118    /// X coordinate
119    pub x: U256,
120    /// Y coordinate
121    pub y: U256,
122}
123
124impl G1Point {
125    /// Create a new G1 point
126    pub fn new(x: U256, y: U256) -> Self {
127        Self { x, y }
128    }
129
130    /// Create from raw bytes (64 bytes: 32 for x, 32 for y)
131    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
132        if bytes.len() != 64 {
133            return None;
134        }
135        let x = U256::from_be_slice(&bytes[0..32]);
136        let y = U256::from_be_slice(&bytes[32..64]);
137        Some(Self { x, y })
138    }
139
140    /// Convert to array format for contract call
141    pub fn to_array(&self) -> [U256; 2] {
142        [self.x, self.y]
143    }
144}
145
146/// BLS G2 point (public key) in contract format
147///
148/// Represented as four 256-bit integers [x0, x1, y0, y1]
149#[derive(Debug, Clone)]
150pub struct G2Point {
151    /// X coordinate (first part)
152    pub x0: U256,
153    /// X coordinate (second part)
154    pub x1: U256,
155    /// Y coordinate (first part)
156    pub y0: U256,
157    /// Y coordinate (second part)
158    pub y1: U256,
159}
160
161impl G2Point {
162    /// Create a new G2 point
163    pub fn new(x0: U256, x1: U256, y0: U256, y1: U256) -> Self {
164        Self { x0, x1, y0, y1 }
165    }
166
167    /// Create from raw bytes (128 bytes: 32 each for x0, x1, y0, y1)
168    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
169        if bytes.len() != 128 {
170            return None;
171        }
172        let x0 = U256::from_be_slice(&bytes[0..32]);
173        let x1 = U256::from_be_slice(&bytes[32..64]);
174        let y0 = U256::from_be_slice(&bytes[64..96]);
175        let y1 = U256::from_be_slice(&bytes[96..128]);
176        Some(Self { x0, x1, y0, y1 })
177    }
178
179    /// Convert to array format for contract call
180    pub fn to_array(&self) -> [U256; 4] {
181        [self.x0, self.x1, self.y0, self.y1]
182    }
183}
184
185/// An aggregated job result ready for submission
186#[derive(Debug, Clone)]
187pub struct AggregatedResult {
188    /// Service ID
189    pub service_id: u64,
190    /// Job call ID
191    pub call_id: u64,
192    /// The job output data
193    pub output: Bytes,
194    /// Bitmap of signers
195    pub signer_bitmap: SignerBitmap,
196    /// Aggregated BLS signature (G1 point)
197    pub signature: G1Point,
198    /// Aggregated BLS public key (G2 point)
199    pub pubkey: G2Point,
200}
201
202impl AggregatedResult {
203    /// Create a new aggregated result
204    pub fn new(
205        service_id: u64,
206        call_id: u64,
207        output: Bytes,
208        signer_bitmap: SignerBitmap,
209        signature: G1Point,
210        pubkey: G2Point,
211    ) -> Self {
212        Self {
213            service_id,
214            call_id,
215            output,
216            signer_bitmap,
217            signature,
218            pubkey,
219        }
220    }
221
222    /// Submit the aggregated result to the Tangle contract
223    ///
224    /// This calls `submitAggregatedResult` on the Tangle contract.
225    pub async fn submit(&self, client: &Arc<TangleClient>) -> Result<(), AggregationError> {
226        blueprint_core::debug!(
227            target: "tangle-aggregation",
228            "Submitting aggregated result for service {} call {} with {} signers",
229            self.service_id,
230            self.call_id,
231            self.signer_bitmap.count_signers()
232        );
233
234        let result = client
235            .submit_aggregated_result(
236                self.service_id,
237                self.call_id,
238                self.output.clone(),
239                self.signer_bitmap.as_u256(),
240                self.signature.to_array(),
241                self.pubkey.to_array(),
242            )
243            .await
244            .map_err(|e| {
245                AggregationError::ContractError(format!("Failed to submit aggregated result: {e}"))
246            })?;
247
248        if result.success {
249            blueprint_core::info!(
250                target: "tangle-aggregation",
251                "Successfully submitted aggregated result for service {} call {} with {} signers: tx_hash={:?}",
252                self.service_id,
253                self.call_id,
254                self.signer_bitmap.count_signers(),
255                result.tx_hash
256            );
257            Ok(())
258        } else {
259            Err(AggregationError::ContractError(format!(
260                "Transaction reverted for service {} call {}: tx_hash={:?}",
261                self.service_id, self.call_id, result.tx_hash
262            )))
263        }
264    }
265}
266
267/// Metadata key for storing job index in job result
268pub const JOB_INDEX_KEY: &str = "tangle.job_index";
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    // ═══════════════════════════════════════════════════════════════════════════
275    // SignerBitmap tests
276    // ═══════════════════════════════════════════════════════════════════════════
277
278    #[test]
279    fn test_signer_bitmap() {
280        let mut bitmap = SignerBitmap::new();
281        assert_eq!(bitmap.count_signers(), 0);
282
283        bitmap.add_signer(0);
284        bitmap.add_signer(2);
285        bitmap.add_signer(5);
286
287        assert!(bitmap.is_signer(0));
288        assert!(!bitmap.is_signer(1));
289        assert!(bitmap.is_signer(2));
290        assert!(!bitmap.is_signer(3));
291        assert!(!bitmap.is_signer(4));
292        assert!(bitmap.is_signer(5));
293        assert_eq!(bitmap.count_signers(), 3);
294
295        bitmap.remove_signer(2);
296        assert!(!bitmap.is_signer(2));
297        assert_eq!(bitmap.count_signers(), 2);
298    }
299
300    #[test]
301    fn test_signer_bitmap_from_indices() {
302        let bitmap = SignerBitmap::from_indices(&[1, 3, 7]);
303        assert!(!bitmap.is_signer(0));
304        assert!(bitmap.is_signer(1));
305        assert!(!bitmap.is_signer(2));
306        assert!(bitmap.is_signer(3));
307        assert!(bitmap.is_signer(7));
308        assert_eq!(bitmap.count_signers(), 3);
309    }
310
311    #[test]
312    fn test_signer_bitmap_empty_indices() {
313        let bitmap = SignerBitmap::from_indices(&[]);
314        assert_eq!(bitmap.count_signers(), 0);
315        assert_eq!(bitmap.as_u256(), U256::ZERO);
316    }
317
318    #[test]
319    fn test_signer_bitmap_large_indices() {
320        // Test with large indices (up to 255 operators supported by U256)
321        let bitmap = SignerBitmap::from_indices(&[0, 100, 200, 255]);
322        assert!(bitmap.is_signer(0));
323        assert!(bitmap.is_signer(100));
324        assert!(bitmap.is_signer(200));
325        assert!(bitmap.is_signer(255));
326        assert!(!bitmap.is_signer(50));
327        assert_eq!(bitmap.count_signers(), 4);
328    }
329
330    #[test]
331    fn test_signer_bitmap_duplicate_indices() {
332        // Adding same index twice should not change count
333        let bitmap = SignerBitmap::from_indices(&[1, 1, 1, 2]);
334        assert_eq!(bitmap.count_signers(), 2);
335    }
336
337    #[test]
338    fn test_signer_bitmap_as_u256() {
339        let bitmap = SignerBitmap::from_indices(&[0, 1, 2]);
340        // Bits 0, 1, 2 set = 0b111 = 7
341        assert_eq!(bitmap.as_u256(), U256::from(7u64));
342    }
343
344    #[test]
345    fn test_signer_bitmap_default() {
346        let bitmap = SignerBitmap::default();
347        assert_eq!(bitmap.count_signers(), 0);
348        assert_eq!(bitmap.as_u256(), U256::ZERO);
349    }
350
351    // ═══════════════════════════════════════════════════════════════════════════
352    // G1Point tests
353    // ═══════════════════════════════════════════════════════════════════════════
354
355    #[test]
356    fn test_g1_point_new() {
357        let x = U256::from(123u64);
358        let y = U256::from(456u64);
359        let point = G1Point::new(x, y);
360        assert_eq!(point.x, x);
361        assert_eq!(point.y, y);
362    }
363
364    #[test]
365    fn test_g1_point_from_bytes() {
366        let mut bytes = [0u8; 64];
367        // Set x = 1 (big endian)
368        bytes[31] = 1;
369        // Set y = 2 (big endian)
370        bytes[63] = 2;
371
372        let point = G1Point::from_bytes(&bytes).expect("should parse 64 bytes");
373        assert_eq!(point.x, U256::from(1u64));
374        assert_eq!(point.y, U256::from(2u64));
375    }
376
377    #[test]
378    fn test_g1_point_from_bytes_invalid_length() {
379        let bytes = [0u8; 32]; // Too short
380        assert!(G1Point::from_bytes(&bytes).is_none());
381
382        let bytes = [0u8; 128]; // Too long
383        assert!(G1Point::from_bytes(&bytes).is_none());
384    }
385
386    #[test]
387    fn test_g1_point_to_array() {
388        let x = U256::from(100u64);
389        let y = U256::from(200u64);
390        let point = G1Point::new(x, y);
391        let arr = point.to_array();
392        assert_eq!(arr[0], x);
393        assert_eq!(arr[1], y);
394    }
395
396    // ═══════════════════════════════════════════════════════════════════════════
397    // G2Point tests
398    // ═══════════════════════════════════════════════════════════════════════════
399
400    #[test]
401    fn test_g2_point_new() {
402        let x0 = U256::from(1u64);
403        let x1 = U256::from(2u64);
404        let y0 = U256::from(3u64);
405        let y1 = U256::from(4u64);
406        let point = G2Point::new(x0, x1, y0, y1);
407        assert_eq!(point.x0, x0);
408        assert_eq!(point.x1, x1);
409        assert_eq!(point.y0, y0);
410        assert_eq!(point.y1, y1);
411    }
412
413    #[test]
414    fn test_g2_point_from_bytes() {
415        let mut bytes = [0u8; 128];
416        // Set x0 = 1, x1 = 2, y0 = 3, y1 = 4 (big endian)
417        bytes[31] = 1;
418        bytes[63] = 2;
419        bytes[95] = 3;
420        bytes[127] = 4;
421
422        let point = G2Point::from_bytes(&bytes).expect("should parse 128 bytes");
423        assert_eq!(point.x0, U256::from(1u64));
424        assert_eq!(point.x1, U256::from(2u64));
425        assert_eq!(point.y0, U256::from(3u64));
426        assert_eq!(point.y1, U256::from(4u64));
427    }
428
429    #[test]
430    fn test_g2_point_from_bytes_invalid_length() {
431        let bytes = [0u8; 64]; // Too short
432        assert!(G2Point::from_bytes(&bytes).is_none());
433
434        let bytes = [0u8; 256]; // Too long
435        assert!(G2Point::from_bytes(&bytes).is_none());
436    }
437
438    #[test]
439    fn test_g2_point_to_array() {
440        let x0 = U256::from(10u64);
441        let x1 = U256::from(20u64);
442        let y0 = U256::from(30u64);
443        let y1 = U256::from(40u64);
444        let point = G2Point::new(x0, x1, y0, y1);
445        let arr = point.to_array();
446        assert_eq!(arr[0], x0);
447        assert_eq!(arr[1], x1);
448        assert_eq!(arr[2], y0);
449        assert_eq!(arr[3], y1);
450    }
451
452    // ═══════════════════════════════════════════════════════════════════════════
453    // AggregatedResult tests
454    // ═══════════════════════════════════════════════════════════════════════════
455
456    #[test]
457    fn test_aggregated_result_new() {
458        let service_id = 1u64;
459        let call_id = 42u64;
460        let output = Bytes::from(vec![1, 2, 3, 4]);
461        let signer_bitmap = SignerBitmap::from_indices(&[0, 1, 2]);
462        let signature = G1Point::new(U256::from(100u64), U256::from(200u64));
463        let pubkey = G2Point::new(
464            U256::from(1u64),
465            U256::from(2u64),
466            U256::from(3u64),
467            U256::from(4u64),
468        );
469
470        let result = AggregatedResult::new(
471            service_id,
472            call_id,
473            output.clone(),
474            signer_bitmap.clone(),
475            signature.clone(),
476            pubkey.clone(),
477        );
478
479        assert_eq!(result.service_id, service_id);
480        assert_eq!(result.call_id, call_id);
481        assert_eq!(result.output, output);
482        assert_eq!(result.signer_bitmap.count_signers(), 3);
483    }
484}