Skip to content

Commit f69be53

Browse files
committed
feat: implement kes validation with genesis key's block test
1 parent 49896d0 commit f69be53

File tree

6 files changed

+285
-123
lines changed

6 files changed

+285
-123
lines changed

common/src/validation.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,19 @@ pub enum KesValidationError {
235235
/// **Cause:** The operational certificate is invalid.
236236
#[error("Operational Certificate Error: {0}")]
237237
OperationalCertificateError(#[from] OperationalCertificateError),
238+
/// **Cause:** No OCert counter found for this issuer (not a stake pool or genesis delegate)
239+
#[error("No OCert Counter For Issuer: Pool ID={}", hex::encode(pool_id))]
240+
NoOCertCounter { pool_id: PoolId },
238241
/// **Cause:** Some data has incorrect bytes
239242
#[error("TryFromSlice: {0}")]
240243
TryFromSlice(String),
241244
#[error("Other Kes Validation Error: {0}")]
242245
Other(String),
243246
}
244247

248+
/// Validation function for Kes
249+
pub type KesValidation<'a> = Box<dyn Fn() -> Result<(), KesValidationError> + Send + Sync + 'a>;
250+
245251
#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
246252
pub enum KesSignatureError {
247253
/// **Cause:** Current KES period is before the operational certificate's
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use acropolis_common::{
2+
crypto::keyhash_224,
3+
validation::{
4+
KesSignatureError, KesValidation, KesValidationError, OperationalCertificateError,
5+
},
6+
GenesisDelegates, PoolId,
7+
};
8+
use imbl::HashMap;
9+
use pallas::{crypto::key::ed25519, ledger::traverse::MultiEraHeader};
10+
11+
use crate::ouroboros::{kes, praos, tpraos};
12+
13+
#[derive(Copy, Clone)]
14+
pub struct OperationalCertificate<'a> {
15+
pub operational_cert_hot_vkey: &'a [u8],
16+
pub operational_cert_sequence_number: u64,
17+
pub operational_cert_kes_period: u64,
18+
pub operational_cert_sigma: &'a [u8],
19+
}
20+
21+
pub fn validate_kes_signature(
22+
slot_kes_period: u64,
23+
opcert_kes_period: u64,
24+
header_body: &[u8],
25+
public_key: &kes::PublicKey,
26+
signature: &kes::Signature,
27+
max_kes_evolutions: u64,
28+
) -> Result<(), KesSignatureError> {
29+
if opcert_kes_period > slot_kes_period {
30+
return Err(KesSignatureError::KesBeforeStartOcert {
31+
ocert_start_period: opcert_kes_period,
32+
current_period: slot_kes_period,
33+
});
34+
}
35+
36+
if slot_kes_period >= opcert_kes_period + max_kes_evolutions {
37+
return Err(KesSignatureError::KesAfterEndOcert {
38+
current_period: slot_kes_period,
39+
ocert_start_period: opcert_kes_period,
40+
max_kes_evolutions,
41+
});
42+
}
43+
44+
let kes_period = (slot_kes_period - opcert_kes_period) as u32;
45+
46+
signature.verify(kes_period, public_key, header_body).map_err(|error| {
47+
KesSignatureError::InvalidKesSignatureOcert {
48+
current_period: slot_kes_period,
49+
ocert_start_period: opcert_kes_period,
50+
reason: error.to_string(),
51+
}
52+
})?;
53+
54+
Ok(())
55+
}
56+
57+
pub fn validate_operational_certificate<'a>(
58+
certificate: OperationalCertificate<'a>,
59+
issuer: &ed25519::PublicKey,
60+
latest_sequence_number: u64,
61+
is_praos: bool,
62+
) -> Result<(), OperationalCertificateError> {
63+
// Verify the Operational Certificate signature
64+
let signature =
65+
ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| {
66+
OperationalCertificateError::MalformedSignatureOcert {
67+
reason: error.to_string(),
68+
}
69+
})?;
70+
71+
let declared_sequence_number = certificate.operational_cert_sequence_number;
72+
73+
// Check the sequence number of the operational certificate. It should either be the same
74+
// as the latest known sequence number for the issuer or one greater.
75+
if declared_sequence_number < latest_sequence_number {
76+
return Err(OperationalCertificateError::CounterTooSmallOcert {
77+
latest_counter: latest_sequence_number,
78+
declared_counter: declared_sequence_number,
79+
});
80+
}
81+
82+
// this is only for praos protocol
83+
if is_praos && (declared_sequence_number - latest_sequence_number) > 1 {
84+
return Err(OperationalCertificateError::CounterOverIncrementedOcert {
85+
latest_counter: latest_sequence_number,
86+
declared_counter: declared_sequence_number,
87+
});
88+
}
89+
90+
// The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period
91+
let mut message = Vec::new();
92+
message.extend_from_slice(certificate.operational_cert_hot_vkey);
93+
message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes());
94+
message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes());
95+
if !issuer.verify(&message, &signature) {
96+
return Err(OperationalCertificateError::InvalidSignatureOcert {
97+
issuer: issuer.as_ref().to_vec(),
98+
});
99+
}
100+
101+
Ok(())
102+
}
103+
104+
pub fn validate_block_kes<'a>(
105+
header: &'a MultiEraHeader,
106+
ocert_counters: &'a HashMap<PoolId, u64>,
107+
active_spos: &'a [PoolId],
108+
genesis_delegs: &'a GenesisDelegates,
109+
slots_per_kes_period: u64,
110+
max_kes_evolutions: u64,
111+
) -> Result<Vec<KesValidation<'a>>, Box<KesValidationError>> {
112+
let is_praos = matches!(header, MultiEraHeader::BabbageCompatible(_));
113+
114+
let issuer_vkey = header.issuer_vkey().ok_or(Box::new(KesValidationError::Other(
115+
"Issuer Key is not set".to_string(),
116+
)))?;
117+
let issuer = ed25519::PublicKey::from(
118+
<[u8; ed25519::PublicKey::SIZE]>::try_from(issuer_vkey)
119+
.map_err(|_| Box::new(KesValidationError::Other("Invalid issuer key".to_string())))?,
120+
);
121+
let pool_id = PoolId::from(keyhash_224(issuer_vkey));
122+
123+
let slot_kes_period = header.slot() / slots_per_kes_period;
124+
let cert = operational_cert(header).ok_or(Box::new(KesValidationError::Other(
125+
"Operational certificate is not set".to_string(),
126+
)))?;
127+
let body_sig = body_signature(header).ok_or(Box::new(KesValidationError::Other(
128+
"Body signature is not set".to_string(),
129+
)))?;
130+
let raw_header_body = header.header_body_cbor().ok_or(Box::new(KesValidationError::Other(
131+
"Header body is not set".to_string(),
132+
)))?;
133+
134+
let latest_sequence_number = if is_praos {
135+
praos::latest_issue_no_praos(ocert_counters, active_spos, &pool_id)
136+
} else {
137+
tpraos::latest_issue_no_tpraos(ocert_counters, active_spos, genesis_delegs, &pool_id)
138+
}
139+
.ok_or(Box::new(KesValidationError::NoOCertCounter { pool_id }))?;
140+
141+
Ok(vec![
142+
Box::new(move || {
143+
validate_kes_signature(
144+
slot_kes_period,
145+
cert.operational_cert_kes_period,
146+
raw_header_body,
147+
&kes::PublicKey::try_from(cert.operational_cert_hot_vkey).map_err(|_| {
148+
KesValidationError::Other(
149+
"Invalid operational certificate hot vkey".to_string(),
150+
)
151+
})?,
152+
&kes::Signature::try_from(body_sig)
153+
.map_err(|_| KesValidationError::Other("Invalid body signature".to_string()))?,
154+
max_kes_evolutions,
155+
)?;
156+
Ok(())
157+
}),
158+
Box::new(move || {
159+
validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?;
160+
Ok(())
161+
}),
162+
])
163+
}
164+
165+
fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option<OperationalCertificate<'a>> {
166+
match header {
167+
MultiEraHeader::ShelleyCompatible(x) => {
168+
let cert = OperationalCertificate {
169+
operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey,
170+
operational_cert_sequence_number: x.header_body.operational_cert_sequence_number,
171+
operational_cert_kes_period: x.header_body.operational_cert_kes_period,
172+
operational_cert_sigma: &x.header_body.operational_cert_sigma,
173+
};
174+
Some(cert)
175+
}
176+
MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate {
177+
operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey,
178+
operational_cert_sequence_number: x
179+
.header_body
180+
.operational_cert
181+
.operational_cert_sequence_number,
182+
operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period,
183+
operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma,
184+
}),
185+
_ => None,
186+
}
187+
}
188+
189+
fn body_signature<'a>(header: &'a MultiEraHeader) -> Option<&'a [u8]> {
190+
match header {
191+
MultiEraHeader::ShelleyCompatible(x) => Some(&x.body_signature),
192+
MultiEraHeader::BabbageCompatible(x) => Some(&x.body_signature),
193+
_ => None,
194+
}
195+
}
196+
197+
#[cfg(test)]
198+
mod tests {
199+
use acropolis_common::{genesis_values::GenesisValues, Era};
200+
use pallas::ledger::traverse::MultiEraHeader;
201+
202+
use super::*;
203+
204+
#[test]
205+
fn test_4490511_block_produced_by_genesis_key() {
206+
let slots_per_kes_period = 129600;
207+
let max_kes_evolutions = 62;
208+
let genesis_values = GenesisValues::mainnet();
209+
210+
let block_header_4490511: Vec<u8> =
211+
hex::decode(include_str!("./data/4490511.cbor")).unwrap();
212+
let block_header =
213+
MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4490511).unwrap();
214+
215+
let ocert_counters = HashMap::new();
216+
let active_spos = Vec::new();
217+
218+
let result = validate_block_kes(
219+
&block_header,
220+
&ocert_counters,
221+
&active_spos,
222+
&genesis_values.genesis_delegs,
223+
slots_per_kes_period,
224+
max_kes_evolutions,
225+
)
226+
.and_then(|kes_validations| {
227+
kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))
228+
});
229+
assert!(result.is_ok());
230+
}
231+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
pub mod kes;
2+
pub mod kes_validation;
23
pub mod praos;
4+
pub mod tpraos;
Lines changed: 13 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,14 @@
1-
use acropolis_common::validation::{KesSignatureError, OperationalCertificateError};
2-
use pallas::crypto::key::ed25519;
3-
4-
use crate::ouroboros::kes;
5-
6-
pub struct OperationalCertificate<'a> {
7-
pub operational_cert_hot_vkey: &'a [u8],
8-
pub operational_cert_sequence_number: u64,
9-
pub operational_cert_kes_period: u64,
10-
pub operational_cert_sigma: &'a [u8],
11-
}
12-
13-
pub fn validate_kes_signature<'a>(
14-
slot_kes_period: u64,
15-
opcert_kes_period: u64,
16-
header_body: &[u8],
17-
public_key: &kes::PublicKey,
18-
signature: &kes::Signature,
19-
max_kes_evolutions: u64,
20-
) -> Result<(), KesSignatureError> {
21-
if opcert_kes_period > slot_kes_period {
22-
return Err(KesSignatureError::KesBeforeStartOcert {
23-
ocert_start_period: opcert_kes_period,
24-
current_period: slot_kes_period,
25-
});
26-
}
27-
28-
if slot_kes_period >= opcert_kes_period + max_kes_evolutions {
29-
return Err(KesSignatureError::KesAfterEndOcert {
30-
current_period: slot_kes_period,
31-
ocert_start_period: opcert_kes_period,
32-
max_kes_evolutions,
33-
});
34-
}
35-
36-
let kes_period = (slot_kes_period - opcert_kes_period) as u32;
37-
38-
signature.verify(kes_period, public_key, header_body).map_err(|error| {
39-
KesSignatureError::InvalidKesSignatureOcert {
40-
current_period: slot_kes_period,
41-
ocert_start_period: opcert_kes_period,
42-
reason: error.to_string(),
43-
}
44-
})?;
45-
46-
Ok(())
47-
}
48-
49-
pub fn validate_operational_certificate<'a>(
50-
certificate: OperationalCertificate<'a>,
51-
issuer: &ed25519::PublicKey,
52-
latest_sequence_number: u64,
53-
is_praos: bool,
54-
) -> Result<(), OperationalCertificateError> {
55-
// Verify the Operational Certificate signature
56-
let signature =
57-
ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| {
58-
OperationalCertificateError::MalformedSignatureOcert {
59-
reason: error.to_string(),
60-
}
61-
})?;
62-
63-
let declared_sequence_number = certificate.operational_cert_sequence_number;
64-
65-
// Check the sequence number of the operational certificate. It should either be the same
66-
// as the latest known sequence number for the issuer or one greater.
67-
if declared_sequence_number < latest_sequence_number {
68-
return Err(OperationalCertificateError::CounterTooSmallOcert {
69-
latest_counter: latest_sequence_number,
70-
declared_counter: declared_sequence_number,
71-
});
72-
}
73-
74-
// this is only for praos protocol
75-
if is_praos && (declared_sequence_number - latest_sequence_number) > 1 {
76-
return Err(OperationalCertificateError::CounterOverIncrementedOcert {
77-
latest_counter: latest_sequence_number,
78-
declared_counter: declared_sequence_number,
79-
});
80-
}
81-
82-
// The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period
83-
let mut message = Vec::new();
84-
message.extend_from_slice(certificate.operational_cert_hot_vkey);
85-
message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes());
86-
message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes());
87-
if !issuer.verify(&message, &signature) {
88-
return Err(OperationalCertificateError::InvalidSignatureOcert {
89-
issuer: issuer.as_ref().to_vec(),
90-
});
91-
}
92-
93-
Ok(())
1+
use acropolis_common::PoolId;
2+
use imbl::HashMap;
3+
4+
pub fn latest_issue_no_praos(
5+
ocert_counter: &HashMap<PoolId, u64>,
6+
active_spos: &[PoolId],
7+
pool_id: &PoolId,
8+
) -> Option<u64> {
9+
ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) {
10+
Some(0)
11+
} else {
12+
None
13+
})
9414
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use acropolis_common::{GenesisDelegates, PoolId};
2+
use imbl::HashMap;
3+
4+
pub fn latest_issue_no_tpraos(
5+
ocert_counter: &HashMap<PoolId, u64>,
6+
active_spos: &[PoolId],
7+
genesis_delegs: &GenesisDelegates,
8+
pool_id: &PoolId,
9+
) -> Option<u64> {
10+
ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) {
11+
Some(0)
12+
} else {
13+
genesis_delegs.as_ref().values().any(|v| v.delegate.eq(pool_id.as_ref())).then_some(0)
14+
})
15+
}

0 commit comments

Comments
 (0)