pessimistic_proof_core/aggchain_data/
multisig.rs1use 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 pub signatures: Vec<Option<Signature>>,
10 pub expected_signers: Vec<Address>,
12 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 pub fn multisig_hash(&self) -> Digest {
42 const ADDRESS_BYTES: usize = 32; const THRESHOLD_BYTES: usize = 32; let mut buf =
46 Vec::with_capacity(THRESHOLD_BYTES + ADDRESS_BYTES * self.expected_signers.len());
47
48 buf.extend(U256::from(self.threshold).to_be_bytes::<32>());
50
51 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 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; };
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}