Skip to main content

onemoney_protocol/utils/
multisig.rs

1//! Multi-signature account utilities.
2//!
3//! This module provides utilities for working with multi-signature accounts:
4//! - Derive multi-sig account addresses from signer configurations
5//! - Aggregate multiple signatures for multi-sig transactions
6//! - Build and validate multi-sig transactions
7
8use alloy_primitives::{Address, keccak256};
9use om_primitives_types::transaction::{B264, MultiSigSignatureEntry, Signature};
10use thiserror::Error;
11
12/// Domain separation tag (DST) for deriving multi-sig account addresses.
13pub const DST_MULTISIG_ADDR_V1: &[u8] = b"MULTISIG_V1";
14
15/// Signer configuration for multi-signature accounts.
16///
17/// Each signer has a public key (33-byte SEC1 compressed format),
18/// a weight, and optional metadata.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct SignerConfig {
21    /// Signer's public key in SEC1 compressed format (33 bytes)
22    pub public_key: Vec<u8>,
23    /// Weight of this signer's vote
24    pub weight: u8,
25}
26
27impl SignerConfig {
28    /// Create a new signer configuration.
29    ///
30    /// # Arguments
31    /// * `public_key` - Public key in SEC1 compressed format (must be 33 bytes)
32    /// * `weight` - Weight of this signer (must be > 0)
33    ///
34    /// # Errors
35    /// Returns error if public key length is not 33 bytes or weight is 0.
36    pub fn new(public_key: Vec<u8>, weight: u8) -> Result<Self, MultiSigError> {
37        if public_key.len() != 33 {
38            return Err(MultiSigError::InvalidPublicKeyLength {
39                expected: 33,
40                actual: public_key.len(),
41            });
42        }
43        if weight == 0 {
44            return Err(MultiSigError::InvalidWeight);
45        }
46        Ok(Self { public_key, weight })
47    }
48}
49
50/// Threshold configuration for multi-signature accounts.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct ThresholdConfig {
53    /// Minimum total weight required for transaction approval
54    pub threshold: u16,
55}
56
57impl ThresholdConfig {
58    /// Create a new threshold configuration.
59    ///
60    /// # Arguments
61    /// * `threshold` - Minimum weight required (must be > 0)
62    ///
63    /// # Errors
64    /// Returns error if threshold is 0.
65    pub fn new(threshold: u16) -> Result<Self, MultiSigError> {
66        if threshold == 0 {
67            return Err(MultiSigError::InvalidThreshold);
68        }
69        Ok(Self { threshold })
70    }
71}
72
73/// Errors that can occur during multi-signature operations.
74#[derive(Debug, Error)]
75pub enum MultiSigError {
76    #[error("invalid public key length: expected {expected} bytes, got {actual} bytes")]
77    InvalidPublicKeyLength { expected: usize, actual: usize },
78
79    #[error("invalid weight: must be greater than 0")]
80    InvalidWeight,
81
82    #[error("invalid threshold: must be greater than 0")]
83    InvalidThreshold,
84
85    #[error("no signers provided")]
86    NoSigners,
87
88    #[error("threshold {threshold} exceeds total weight {total_weight}")]
89    ThresholdExceedsTotalWeight { threshold: u16, total_weight: u16 },
90
91    #[error("duplicate public key found")]
92    DuplicatePublicKey,
93
94    #[error("signature count mismatch: expected at least 1, got {count}")]
95    InsufficientSignatures { count: usize },
96}
97
98/// Derive a multi-signature account address from signer configurations.
99///
100/// The address is deterministically derived by:
101/// 1. Sorting signers by public key (lexicographic order)
102/// 2. Concatenating: pubkey1 || weight1 || pubkey2 || weight2 || ... ||
103///    threshold
104/// 3. Computing keccak256 hash
105/// 4. Taking last 20 bytes as address
106///
107/// # Arguments
108/// * `signers` - List of signer configurations
109/// * `threshold` - Threshold configuration
110///
111/// # Returns
112/// The derived multi-sig account address
113///
114/// # Errors
115/// Returns error if:
116/// - No signers provided
117/// - Threshold exceeds total weight
118/// - Duplicate public keys found
119///
120/// # Example
121/// ```
122/// use onemoney_protocol::utils::{SignerConfig, ThresholdConfig, derive_multisig_address};
123///
124/// let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
125/// let signer2 = SignerConfig::new(vec![3; 33], 1).unwrap();
126/// let threshold = ThresholdConfig::new(2).unwrap();
127///
128/// let address = derive_multisig_address(&[signer1, signer2], &threshold).unwrap();
129/// ```
130pub fn derive_multisig_address(
131    signers: &[SignerConfig],
132    threshold: &ThresholdConfig,
133) -> Result<Address, MultiSigError> {
134    if signers.is_empty() {
135        return Err(MultiSigError::NoSigners);
136    }
137
138    if threshold.threshold == 0 {
139        return Err(MultiSigError::InvalidThreshold);
140    }
141
142    for signer in signers {
143        if signer.weight == 0 {
144            return Err(MultiSigError::InvalidWeight);
145        }
146        if signer.public_key.len() != 33 {
147            return Err(MultiSigError::InvalidPublicKeyLength {
148                expected: 33,
149                actual: signer.public_key.len(),
150            });
151        }
152    }
153
154    // Calculate total weight
155    let total_weight: u16 = signers.iter().map(|s| s.weight as u16).sum();
156    if threshold.threshold > total_weight {
157        return Err(MultiSigError::ThresholdExceedsTotalWeight {
158            threshold: threshold.threshold,
159            total_weight,
160        });
161    }
162
163    // Sort signers by public key for deterministic ordering
164    let mut sorted_signers: Vec<&SignerConfig> = signers.iter().collect();
165    sorted_signers.sort_by(|a, b| a.public_key.cmp(&b.public_key));
166
167    // Check for duplicates
168    for i in 1..sorted_signers.len() {
169        if sorted_signers[i].public_key == sorted_signers[i - 1].public_key {
170            return Err(MultiSigError::DuplicatePublicKey);
171        }
172    }
173
174    // Serialize configuration: pubkey || weight || pubkey || weight || ... ||
175    // threshold
176    let mut data = Vec::with_capacity(DST_MULTISIG_ADDR_V1.len() + signers.len() * 34 + 2);
177    data.extend_from_slice(DST_MULTISIG_ADDR_V1);
178    for signer in &sorted_signers {
179        data.extend_from_slice(&signer.public_key);
180        data.push(signer.weight);
181    }
182    data.extend_from_slice(&threshold.threshold.to_be_bytes());
183
184    // Hash and take last 20 bytes
185    let hash = keccak256(&data);
186    let mut address_bytes = [0u8; 20];
187    address_bytes.copy_from_slice(&hash[12..32]);
188
189    Ok(Address::from(address_bytes))
190}
191
192/// Multi-signature transaction signature collector.
193///
194/// Helps collect signatures from multiple signers for multi-sig transactions.
195/// After collecting signatures, use `Signed::new_multi_sig` to create the
196/// signed transaction.
197///
198/// # Example
199/// ```no_run
200/// use alloy_primitives::Address;
201/// use onemoney_protocol::{
202///     MultiSigSignatureEntry, PaymentPayload, Signature, utils::MultiSigSignatureCollector,
203/// };
204///
205/// let mut collector = MultiSigSignatureCollector::new();
206/// collector.add_signature(signer_pubkey1, signature1);
207/// collector.add_signature(signer_pubkey2, signature2);
208///
209/// // Get collected signatures
210/// let signatures = collector.signatures();
211///
212/// // User creates Signed transaction:
213/// // let signed = Signed::new_multi_sig(payload, multisig_account, signatures);
214/// ```
215pub struct MultiSigSignatureCollector {
216    signatures: Vec<MultiSigSignatureEntry>,
217}
218
219impl MultiSigSignatureCollector {
220    /// Create a new signature collector.
221    pub fn new() -> Self {
222        Self { signatures: Vec::new() }
223    }
224
225    /// Add a signature from a signer.
226    ///
227    /// # Arguments
228    /// * `signer_pubkey` - Signer's public key (33 bytes SEC1 compressed)
229    /// * `signature` - Signature from this signer
230    ///
231    /// # Returns
232    /// Mutable reference to self for method chaining
233    pub fn add_signature(&mut self, signer_pubkey: B264, signature: Signature) -> &mut Self {
234        self.signatures.push(MultiSigSignatureEntry {
235            signer_pubkey,
236            signature,
237        });
238        self
239    }
240
241    /// Add multiple signatures at once.
242    ///
243    /// # Arguments
244    /// * `signatures` - Vector of signature entries
245    ///
246    /// # Returns
247    /// Mutable reference to self for method chaining
248    pub fn add_signatures(&mut self, mut signatures: Vec<MultiSigSignatureEntry>) -> &mut Self {
249        self.signatures.append(&mut signatures);
250        self
251    }
252
253    /// Get the number of signatures collected so far.
254    pub fn signature_count(&self) -> usize {
255        self.signatures.len()
256    }
257
258    /// Get all collected signatures.
259    ///
260    /// # Returns
261    /// Vector of all signature entries
262    pub fn signatures(self) -> Vec<MultiSigSignatureEntry> {
263        self.signatures
264    }
265
266    /// Check if any signatures have been collected.
267    pub fn is_empty(&self) -> bool {
268        self.signatures.is_empty()
269    }
270}
271
272impl Default for MultiSigSignatureCollector {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_signer_config_new_valid() {
284        let pubkey = vec![2; 33];
285        let signer = SignerConfig::new(pubkey.clone(), 1).unwrap();
286        assert_eq!(signer.public_key, pubkey);
287        assert_eq!(signer.weight, 1);
288    }
289
290    #[test]
291    fn test_signer_config_invalid_pubkey_length() {
292        let pubkey = vec![2; 32]; // Wrong length
293        let result = SignerConfig::new(pubkey, 1);
294        assert!(matches!(result, Err(MultiSigError::InvalidPublicKeyLength { .. })));
295    }
296
297    #[test]
298    fn test_signer_config_zero_weight() {
299        let pubkey = vec![2; 33];
300        let result = SignerConfig::new(pubkey, 0);
301        assert!(matches!(result, Err(MultiSigError::InvalidWeight)));
302    }
303
304    #[test]
305    fn test_threshold_config_new_valid() {
306        let threshold = ThresholdConfig::new(2).unwrap();
307        assert_eq!(threshold.threshold, 2);
308    }
309
310    #[test]
311    fn test_threshold_config_zero() {
312        let result = ThresholdConfig::new(0);
313        assert!(matches!(result, Err(MultiSigError::InvalidThreshold)));
314    }
315
316    #[test]
317    fn test_derive_multisig_address_2_of_3() {
318        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
319        let signer2 = SignerConfig::new(vec![3; 33], 1).unwrap();
320        let signer3 = SignerConfig::new(vec![4; 33], 1).unwrap();
321        let threshold = ThresholdConfig::new(2).unwrap();
322
323        let address =
324            derive_multisig_address(&[signer1.clone(), signer2.clone(), signer3.clone()], &threshold).unwrap();
325
326        // Address should be deterministic
327        let address2 = derive_multisig_address(&[signer3, signer1, signer2], &threshold).unwrap();
328        assert_eq!(
329            address, address2,
330            "Address should be deterministic regardless of input order"
331        );
332    }
333
334    #[test]
335    fn test_derive_multisig_address_no_signers() {
336        let threshold = ThresholdConfig::new(1).unwrap();
337        let result = derive_multisig_address(&[], &threshold);
338        assert!(matches!(result, Err(MultiSigError::NoSigners)));
339    }
340
341    #[test]
342    fn test_derive_multisig_address_threshold_exceeds_weight() {
343        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
344        let signer2 = SignerConfig::new(vec![3; 33], 1).unwrap();
345        let threshold = ThresholdConfig::new(3).unwrap(); // Total weight is 2
346
347        let result = derive_multisig_address(&[signer1, signer2], &threshold);
348        assert!(matches!(result, Err(MultiSigError::ThresholdExceedsTotalWeight { .. })));
349    }
350
351    #[test]
352    fn test_derive_multisig_address_duplicate_pubkey() {
353        let signer1 = SignerConfig::new(vec![2; 33], 1).unwrap();
354        let signer2 = SignerConfig::new(vec![2; 33], 2).unwrap(); // Same pubkey
355        let threshold = ThresholdConfig::new(2).unwrap();
356
357        let result = derive_multisig_address(&[signer1, signer2], &threshold);
358        assert!(matches!(result, Err(MultiSigError::DuplicatePublicKey)));
359    }
360
361    #[test]
362    fn test_multisig_collector() {
363        let mut collector = MultiSigSignatureCollector::new();
364
365        assert_eq!(collector.signature_count(), 0);
366        assert!(collector.is_empty());
367
368        let sig1 = Signature::test_signature();
369        let sig2 = Signature::test_signature();
370
371        collector.add_signature(B264::repeat_byte(2), sig1);
372        collector.add_signature(B264::repeat_byte(3), sig2);
373
374        assert_eq!(collector.signature_count(), 2);
375        assert!(!collector.is_empty());
376
377        let signatures = collector.signatures();
378        assert_eq!(signatures.len(), 2);
379    }
380
381    #[test]
382    fn test_multisig_collector_default() {
383        let collector = MultiSigSignatureCollector::default();
384        assert!(collector.is_empty());
385        assert_eq!(collector.signature_count(), 0);
386    }
387}