pessimistic_proof_core/aggchain_data/
multisig.rs

1use agglayer_primitives::{Address, Digest, Signature};
2use alloy_primitives::{keccak256, B256, U256};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct MultiSignature {
8    /// Set of the indexed signatures
9    pub signatures: Vec<Option<Signature>>,
10    /// Set of all registered signers
11    pub expected_signers: Vec<Address>,
12    /// Inclusive minimal number of signers.
13    pub threshold: usize,
14}
15
16#[derive(Clone, Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
17pub enum MultisigError {
18    #[error("Too many signers provided. got: {num}, committee size: {max}.")]
19    TooManySigners { num: usize, max: usize },
20
21    #[error("Multisig is under the required threshold. got: {got}, expected: {expected}")]
22    UnderThreshold { got: usize, expected: usize },
23
24    #[error("Signature claimed to be from signer {idx} is invalid.")]
25    InvalidSignature { idx: usize },
26
27    #[error(
28        "Signature #{idx} is wrong. Expected signer {expected_signer}, but commitment \
29         {commitment} recovers signer {recovered_signer}."
30    )]
31    InvalidSigner {
32        idx: usize,
33        expected_signer: Address,
34        recovered_signer: Address,
35        commitment: Digest,
36    },
37}
38
39impl MultiSignature {
40    /// Commitment on the signers and threshold.
41    pub fn multisig_hash(&self) -> Digest {
42        const ADDRESS_BYTES: usize = 32; // 32-bytes because padded 20bytes address
43        const THRESHOLD_BYTES: usize = 32; // 32-bytes
44
45        let mut buf =
46            Vec::with_capacity(THRESHOLD_BYTES + ADDRESS_BYTES * self.expected_signers.len());
47
48        // 32-bytes threshold
49        buf.extend(U256::from(self.threshold).to_be_bytes::<32>());
50
51        // 20-bytes per signer (padded in 32bytes)
52        for a in &self.expected_signers {
53            buf.extend_from_slice(&[0u8; 12]);
54            buf.extend_from_slice(&a.into_array());
55        }
56
57        keccak256(&buf).into()
58    }
59
60    /// Verify signatures and ensure they are all from the expected set.
61    pub fn verify(&self, commitment: B256) -> Result<(), MultisigError> {
62        if self.signatures.len() > self.expected_signers.len() {
63            return Err(MultisigError::TooManySigners {
64                num: self.signatures.len(),
65                max: self.expected_signers.len(),
66            });
67        }
68
69        let nb_signatures = self.signatures.iter().filter(|s| s.is_some()).count();
70        if nb_signatures < self.threshold {
71            return Err(MultisigError::UnderThreshold {
72                got: nb_signatures,
73                expected: self.threshold,
74            });
75        }
76
77        for (idx, signature) in self.signatures.iter().enumerate() {
78            let Some(signature) = signature else {
79                continue; // No signature is a valid signature
80            };
81            let recovered_signer = signature
82                .recover_address_from_prehash(&commitment)
83                .map_err(|_| MultisigError::InvalidSignature { idx })?;
84            let expected_signer = self.expected_signers[idx];
85            if recovered_signer != expected_signer {
86                return Err(MultisigError::InvalidSigner {
87                    idx,
88                    expected_signer,
89                    recovered_signer,
90                    commitment: Digest::from(commitment),
91                });
92            }
93        }
94
95        Ok(())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use alloy::{
102        primitives::keccak256,
103        signers::{local::PrivateKeySigner, SignerSync as _},
104    };
105    use rstest::rstest;
106
107    use super::*;
108
109    fn prehash() -> B256 {
110        let h = keccak256(b"prehash");
111        B256::new(h.0)
112    }
113
114    fn wallet(i: usize) -> PrivateKeySigner {
115        let seed = keccak256(i.to_be_bytes());
116        PrivateKeySigner::from_slice(seed.as_slice()).unwrap()
117    }
118
119    #[rstest]
120    #[case(vec![true, true], 2, Ok(()))]
121    #[case(vec![true, false], 2, Err(MultisigError::UnderThreshold { got: 1, expected: 2 }))]
122    #[case(vec![false, false, true], 1, Err(MultisigError::TooManySigners { num: 3, max: 2 }))]
123    fn verify_cases(
124        #[case] signers: Vec<bool>,
125        #[case] threshold: usize,
126        #[case] expected: Result<(), MultisigError>,
127    ) {
128        let wallets: Vec<PrivateKeySigner> = (0..3).map(wallet).collect();
129        let prehash = prehash();
130
131        let expected_signers: Vec<Address> = wallets
132            .iter()
133            .take(2)
134            .map(|sk| sk.address().into())
135            .collect();
136
137        let signatures: Vec<Option<Signature>> = signers
138            .iter()
139            .enumerate()
140            .map(|(idx, enabled)| {
141                enabled.then(|| wallets[idx].sign_hash_sync(&prehash).unwrap().into())
142            })
143            .collect();
144
145        let ms = MultiSignature {
146            signatures,
147            expected_signers,
148            threshold,
149        };
150
151        assert_eq!(ms.verify(prehash).map(|_| ()), expected);
152    }
153}