Skip to content

Commit 7e72248

Browse files
authored
feat: Multiple Block Signatures (#3174)
* Initial sig impl * Add as_keys() * Refactor BlockSignatures * Lint * Move verification to sig construction * Add min sig check to constructor * Use vec instead of constant * Remove invariants from new() * Dedupe test fns * Changelog * Fix changelog * Use with_capacity * Max 5 validators * Move changelog line * RM assert_signatures_verify * Bump crossbeam-epoch for cargo deny check * Check block sigs count
1 parent 16377c8 commit 7e72248

16 files changed

Lines changed: 941 additions & 291 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# Changelog
2-
32
## v0.16.0 (TBD)
43

54
### Changes
@@ -87,6 +86,7 @@
8786
- Documented that the `ecdsa_k256_keccak` authentication scheme discloses the signer's public key and signature at proving time via precompile calldata ([#3178](https://github.com/0xMiden/protocol/pull/3178)).
8887
- Renamed the `Authority` config value slot, expressed the authority kind as a MASM `enum Authority : u8`, and enforced the canonical config-word encoding on read ([#3209](https://github.com/0xMiden/protocol/pull/3209)).
8988
- Unified procedure ordering and document sender-based access control's authentication assumption in the `ownable2step` and `rbac` access control modules ([#3205](https://github.com/0xMiden/protocol/pull/3205)).
89+
- [BREAKING] Updated `BlockHeader` to support multiple validator keys and added `ValidatorKeys` and `BlockSignatures` types ([#3174](https://github.com/0xMiden/protocol/pull/3174)).
9090

9191
### Fixes
9292

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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

Comments
 (0)