|
| 1 | +use alloc::vec::Vec; |
| 2 | + |
| 3 | +use miden_crypto::dsa::ecdsa_k256_keccak::Signature; |
| 4 | + |
| 5 | +use crate::Word; |
| 6 | +use crate::block::ValidatorKeys; |
| 7 | +use crate::utils::serde::{ |
| 8 | + ByteReader, |
| 9 | + ByteWriter, |
| 10 | + Deserializable, |
| 11 | + DeserializationError, |
| 12 | + Serializable, |
| 13 | +}; |
| 14 | + |
| 15 | +// SIGNATURE VERIFICATION ERROR |
| 16 | +// ================================================================================================ |
| 17 | + |
| 18 | +/// Error returned when verifying [`BlockSignatures`] against a validator set (see |
| 19 | +/// [`BlockSignatures::verify_against`]). |
| 20 | +#[derive(Debug, thiserror::Error)] |
| 21 | +#[non_exhaustive] |
| 22 | +pub enum SignatureVerificationError { |
| 23 | + #[error("block has {actual} signatures but the validator set has {expected} keys")] |
| 24 | + SignatureCountMismatch { expected: usize, actual: usize }, |
| 25 | + #[error( |
| 26 | + "block signature at position {position} does not verify against the validator key at that position" |
| 27 | + )] |
| 28 | + InvalidSignatureAtPosition { position: usize }, |
| 29 | +} |
| 30 | + |
| 31 | +// BLOCK SIGNATURES ERROR |
| 32 | +// ================================================================================================ |
| 33 | + |
| 34 | +/// Error returned when constructing an invalid [`BlockSignatures`] set. |
| 35 | +#[derive(Debug, thiserror::Error)] |
| 36 | +#[non_exhaustive] |
| 37 | +pub enum BlockSignaturesError { |
| 38 | + #[error( |
| 39 | + "block signature set contains {count} signatures but must contain at most {max}", |
| 40 | + max = ValidatorKeys::MAX, |
| 41 | + )] |
| 42 | + TooManySignatures { count: usize }, |
| 43 | +} |
| 44 | + |
| 45 | +// BLOCK SIGNATURES |
| 46 | +// ================================================================================================ |
| 47 | + |
| 48 | +/// The set of validator signatures over a block header ordered by validator key. |
| 49 | +/// |
| 50 | +/// The signatures are expected to be ordered with respect to a validator set (see |
| 51 | +/// [`ValidatorKeys`]): the signature in slot `i` is produced by, and verified against, the |
| 52 | +/// validator key at index `i`. Every validator in the set must sign; there is no partial-signing or |
| 53 | +/// threshold support. |
| 54 | +/// |
| 55 | +/// This is a plain, unchecked container: neither [`BlockSignatures::new`] nor deserialization |
| 56 | +/// verify anything about the signatures they hold. The only way to establish that a |
| 57 | +/// [`BlockSignatures`] value is valid is to call [`BlockSignatures::verify_against`]. |
| 58 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 59 | +pub struct BlockSignatures { |
| 60 | + /// Positional signatures; `signatures[i]` corresponds to validator key `i`. |
| 61 | + signatures: Vec<Signature>, |
| 62 | +} |
| 63 | + |
| 64 | +impl BlockSignatures { |
| 65 | + // CONSTRUCTORS |
| 66 | + // -------------------------------------------------------------------------------------------- |
| 67 | + |
| 68 | + /// Returns a new [`BlockSignatures`] from the provided signatures, ordered positionally. |
| 69 | + /// |
| 70 | + /// This performs no validation beyond checking that the number of signatures does not exceed |
| 71 | + /// [`ValidatorKeys::MAX`]: the caller is responsible for ordering `signatures` to align |
| 72 | + /// with the validator set it is meant to be checked against. Call |
| 73 | + /// [`BlockSignatures::verify_against`] to establish that the signatures are valid. |
| 74 | + pub fn new(signatures: Vec<Signature>) -> Result<Self, BlockSignaturesError> { |
| 75 | + if signatures.len() > ValidatorKeys::MAX { |
| 76 | + return Err(BlockSignaturesError::TooManySignatures { count: signatures.len() }); |
| 77 | + } |
| 78 | + Ok(Self { signatures }) |
| 79 | + } |
| 80 | + |
| 81 | + // PUBLIC ACCESSORS |
| 82 | + // -------------------------------------------------------------------------------------------- |
| 83 | + |
| 84 | + /// Returns the positional signatures, where signature `i` corresponds to validator key `i`. |
| 85 | + pub fn as_signatures(&self) -> &[Signature] { |
| 86 | + &self.signatures |
| 87 | + } |
| 88 | + |
| 89 | + /// Returns the number of signatures. |
| 90 | + pub fn len(&self) -> usize { |
| 91 | + self.signatures.len() |
| 92 | + } |
| 93 | + |
| 94 | + /// Returns `true` if there are no signatures. |
| 95 | + pub fn is_empty(&self) -> bool { |
| 96 | + self.signatures.is_empty() |
| 97 | + } |
| 98 | + |
| 99 | + // VERIFICATION |
| 100 | + // -------------------------------------------------------------------------------------------- |
| 101 | + |
| 102 | + /// Verifies the signatures positionally against `validator_keys` over `block_commitment`. |
| 103 | + /// |
| 104 | + /// This is the canonical verification of an ordered signature set, and the only place that |
| 105 | + /// establishes a [`BlockSignatures`] value is valid: the number of signatures must match the |
| 106 | + /// number of validator keys, and the signature in slot `i` must verify against the validator |
| 107 | + /// key at index `i`. |
| 108 | + /// |
| 109 | + /// # Errors |
| 110 | + /// |
| 111 | + /// Returns an error if the number of signatures does not match the number of validator keys, or |
| 112 | + /// if a signature does not verify against the validator key at its position. |
| 113 | + pub fn verify_against( |
| 114 | + &self, |
| 115 | + block_commitment: Word, |
| 116 | + validator_keys: &ValidatorKeys, |
| 117 | + ) -> Result<(), SignatureVerificationError> { |
| 118 | + if self.signatures.len() != validator_keys.len() { |
| 119 | + return Err(SignatureVerificationError::SignatureCountMismatch { |
| 120 | + expected: validator_keys.len(), |
| 121 | + actual: self.signatures.len(), |
| 122 | + }); |
| 123 | + } |
| 124 | + |
| 125 | + for (position, (signature, validator_key)) in |
| 126 | + self.signatures.iter().zip(validator_keys.as_keys()).enumerate() |
| 127 | + { |
| 128 | + if !signature.verify(block_commitment, validator_key) { |
| 129 | + return Err(SignatureVerificationError::InvalidSignatureAtPosition { position }); |
| 130 | + } |
| 131 | + } |
| 132 | + |
| 133 | + Ok(()) |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +// SERIALIZATION |
| 138 | +// ================================================================================================ |
| 139 | + |
| 140 | +impl Serializable for BlockSignatures { |
| 141 | + fn write_into<W: ByteWriter>(&self, target: &mut W) { |
| 142 | + self.signatures.write_into(target); |
| 143 | + } |
| 144 | +} |
| 145 | + |
| 146 | +impl Deserializable for BlockSignatures { |
| 147 | + fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> { |
| 148 | + let signatures = Vec::<Signature>::read_from(source)?; |
| 149 | + Ok(Self { signatures }) |
| 150 | + } |
| 151 | +} |
| 152 | + |
| 153 | +// TESTS |
| 154 | +// ================================================================================================ |
| 155 | + |
| 156 | +#[cfg(test)] |
| 157 | +mod tests { |
| 158 | + use super::*; |
| 159 | + use crate::testing::random_secret_key::random_secret_key; |
| 160 | + use crate::testing::validator_keys::{random_validator_set, sign_all}; |
| 161 | + |
| 162 | + #[test] |
| 163 | + fn verify_against_accepts_correctly_ordered_signatures() { |
| 164 | + let (signers, keys) = random_validator_set(5); |
| 165 | + let commitment = Word::empty(); |
| 166 | + |
| 167 | + let signatures = sign_all(&keys, &signers, commitment); |
| 168 | + |
| 169 | + assert_eq!(signatures.len(), 5); |
| 170 | + signatures.verify_against(commitment, &keys).unwrap(); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn verify_against_rejects_invalid_signature() { |
| 175 | + let (signers, keys) = random_validator_set(3); |
| 176 | + let commitment = Word::empty(); |
| 177 | + let outsider = random_secret_key(); |
| 178 | + |
| 179 | + // Position 1 holds a signature that does not verify against the key committed there. |
| 180 | + let mut signatures = sign_all(&keys, &signers, commitment).as_signatures().to_vec(); |
| 181 | + signatures[1] = outsider.sign(commitment); |
| 182 | + let signatures = BlockSignatures::new(signatures).unwrap(); |
| 183 | + |
| 184 | + assert!(matches!( |
| 185 | + signatures.verify_against(commitment, &keys), |
| 186 | + Err(SignatureVerificationError::InvalidSignatureAtPosition { position: 1 }) |
| 187 | + )); |
| 188 | + } |
| 189 | + |
| 190 | + #[test] |
| 191 | + fn verify_against_rejects_mismatched_keys() { |
| 192 | + let (signers, keys) = random_validator_set(3); |
| 193 | + let commitment = Word::empty(); |
| 194 | + let signatures = sign_all(&keys, &signers, commitment); |
| 195 | + |
| 196 | + // The same, fully valid set does not verify against a different validator set of the same |
| 197 | + // size. |
| 198 | + let (_, other_keys) = random_validator_set(3); |
| 199 | + assert!(matches!( |
| 200 | + signatures.verify_against(commitment, &other_keys), |
| 201 | + Err(SignatureVerificationError::InvalidSignatureAtPosition { .. }) |
| 202 | + )); |
| 203 | + } |
| 204 | + |
| 205 | + #[test] |
| 206 | + fn verify_against_rejects_count_mismatch() { |
| 207 | + let (signers, keys) = random_validator_set(3); |
| 208 | + let commitment = Word::empty(); |
| 209 | + let signatures = sign_all(&keys, &signers, commitment); |
| 210 | + |
| 211 | + // A validator set of a different size cannot align positionally. |
| 212 | + let (_, other_keys) = random_validator_set(4); |
| 213 | + assert!(matches!( |
| 214 | + signatures.verify_against(commitment, &other_keys), |
| 215 | + Err(SignatureVerificationError::SignatureCountMismatch { expected: 4, actual: 3 }) |
| 216 | + )); |
| 217 | + } |
| 218 | + |
| 219 | + #[test] |
| 220 | + fn serde_round_trip() { |
| 221 | + let (signers, keys) = random_validator_set(3); |
| 222 | + let commitment = Word::empty(); |
| 223 | + let signatures = sign_all(&keys, &signers, commitment); |
| 224 | + |
| 225 | + let bytes = signatures.to_bytes(); |
| 226 | + let deserialized = BlockSignatures::read_from_bytes(&bytes).unwrap(); |
| 227 | + assert_eq!(signatures, deserialized); |
| 228 | + } |
| 229 | +} |
0 commit comments