Skip to content

Commit 19e960e

Browse files
committed
feat: add kes ocert validation
1 parent 17e6b26 commit 19e960e

File tree

6 files changed

+139
-25
lines changed

6 files changed

+139
-25
lines changed

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/validation.rs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,12 @@ impl PartialEq for BadVrfProofError {
229229
/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342
230230
#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
231231
pub enum KesValidationError {
232-
#[error("{0}")]
232+
#[error("KES Signature Error: {0}")]
233233
KesSignatureError(#[from] KesSignatureError),
234-
#[error("{0}")]
234+
#[error("Operational Certificate Error: {0}")]
235235
OperationalCertificateError(#[from] OperationalCertificateError),
236+
#[error("Other Kes Validation Error: {0}")]
237+
Other(String),
236238
}
237239

238240
#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
@@ -274,36 +276,35 @@ pub enum KesSignatureError {
274276

275277
#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
276278
pub enum OperationalCertificateError {
279+
/// **Cause:** The operational certificate is malformed.
280+
#[error("Malformed Signature OCert: Reason={}", reason)]
281+
MalformedSignatureOcert { reason: String },
282+
/// **Cause:** The cold key signature on the operational certificate is invalid.
283+
/// The OCert was not properly signed by the pool's cold key.
284+
#[error("Invalid Signature OCert: Issuer={}", hex::encode(issuer))]
285+
InvalidSignatureOcert { issuer: Vec<u8> },
277286
/// **Cause:** The operational certificate counter in the header is not greater
278287
/// than the last counter used by this pool.
279288
#[error(
280-
"Counter Too Small OCert: Last Counter={}, Current Counter={}",
281-
last_counter,
282-
current_counter
289+
"Counter Too Small OCert: Latest Counter={}, Declared Counter={}",
290+
latest_counter,
291+
declared_counter
283292
)]
284293
CounterTooSmallOcert {
285-
last_counter: u64,
286-
current_counter: u64,
294+
latest_counter: u64,
295+
declared_counter: u64,
287296
},
288297
/// **Cause:** OCert counter jumped by more than 1. While not strictly invalid,
289298
/// this is suspicious and may indicate key compromise. (Praos Only)
290299
#[error(
291-
"Counter Over Incremented OCert: Last Counter={}, Current Counter={}",
292-
last_counter,
293-
current_counter
300+
"Counter Over Incremented OCert: Latest Counter={}, Declared Counter={}",
301+
latest_counter,
302+
declared_counter
294303
)]
295304
CounterOverIncrementedOcert {
296-
last_counter: u64,
297-
current_counter: u64,
305+
latest_counter: u64,
306+
declared_counter: u64,
298307
},
299-
/// **Cause:** The cold key signature on the operational certificate is invalid.
300-
/// The OCert was not properly signed by the pool's cold key.
301-
#[error(
302-
"Invalid Signature OCert: Counter={}, KES Period={}",
303-
counter,
304-
kes_period
305-
)]
306-
InvalidSignatureOcert { counter: u64, kes_period: u64 },
307308
/// **Cause:** No counter found for this key hash (not a stake pool or genesis delegate)
308309
#[error("No Counter For Key Hash OCert: Pool ID={}", hex::encode(pool_id))]
309310
NoCounterForKeyHashOcert { pool_id: PoolId },

modules/block_kes_validator/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ tokio = { workspace = true }
2121
tracing = { workspace = true }
2222
serde_json = { workspace = true }
2323
serde = { workspace = true }
24-
blake2 = "0.10.6"
25-
num-traits = "0.2"
2624
thiserror = "2.0.17"
25+
pallas = { workspace = true }
2726

2827
kes-summed-ed25519 = { git = "https://github.com/txpipe/kes", rev = "f69fb357d46f6a18925543d785850059569d7e78" }
2928

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
mod kes;
1+
pub mod kes;
2+
pub mod praos;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use acropolis_common::validation::OperationalCertificateError;
2+
use pallas::crypto::key::ed25519;
3+
4+
pub struct OperationalCertificate<'a> {
5+
pub operational_cert_hot_vkey: &'a [u8],
6+
pub operational_cert_sequence_number: u64,
7+
pub operational_cert_kes_period: u64,
8+
pub operational_cert_sigma: &'a [u8],
9+
}
10+
11+
pub fn validate_operational_certificate<'a>(
12+
certificate: OperationalCertificate<'a>,
13+
issuer: &ed25519::PublicKey,
14+
latest_sequence_number: u64,
15+
is_praos: bool,
16+
) -> Result<(), OperationalCertificateError> {
17+
// Verify the Operational Certificate signature
18+
let signature =
19+
ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| {
20+
OperationalCertificateError::MalformedSignatureOcert {
21+
reason: error.to_string(),
22+
}
23+
})?;
24+
25+
let declared_sequence_number = certificate.operational_cert_sequence_number;
26+
27+
// Check the sequence number of the operational certificate. It should either be the same
28+
// as the latest known sequence number for the issuer or one greater.
29+
if declared_sequence_number < latest_sequence_number {
30+
return Err(OperationalCertificateError::CounterTooSmallOcert {
31+
latest_counter: latest_sequence_number,
32+
declared_counter: declared_sequence_number,
33+
});
34+
}
35+
36+
// this is only for praos protocol
37+
if is_praos && (declared_sequence_number - latest_sequence_number) > 1 {
38+
return Err(OperationalCertificateError::CounterOverIncrementedOcert {
39+
latest_counter: latest_sequence_number,
40+
declared_counter: declared_sequence_number,
41+
});
42+
}
43+
44+
// The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period
45+
let mut message = Vec::new();
46+
message.extend_from_slice(certificate.operational_cert_hot_vkey);
47+
message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes());
48+
message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes());
49+
if !issuer.verify(&message, &signature) {
50+
return Err(OperationalCertificateError::InvalidSignatureOcert {
51+
issuer: issuer.as_ref().to_vec(),
52+
});
53+
}
54+
55+
Ok(())
56+
}

modules/block_kes_validator/src/state.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
use crate::ouroboros::praos::OperationalCertificate;
12
use acropolis_common::{
23
genesis_values::GenesisValues, messages::ProtocolParamsMessage, validation::KesValidationError,
34
BlockInfo, PoolId,
45
};
56
use imbl::HashMap;
7+
use pallas::ledger::{primitives::babbage::OperationalCert, traverse::MultiEraHeader};
8+
use tracing::error;
69

710
#[derive(Default, Debug, Clone)]
811
pub struct State {
@@ -35,6 +38,61 @@ impl State {
3538
raw_header: &[u8],
3639
genesis: &GenesisValues,
3740
) -> Result<(), Box<KesValidationError>> {
41+
// Validation starts after Shelley Era
42+
if block_info.epoch < genesis.shelley_epoch {
43+
return Ok(());
44+
}
45+
46+
let header = match MultiEraHeader::decode(block_info.era as u8, None, raw_header) {
47+
Ok(header) => header,
48+
Err(e) => {
49+
error!("Can't decode header {}: {e}", block_info.slot);
50+
return Err(Box::new(KesValidationError::Other(format!(
51+
"Can't decode header {}: {e}",
52+
block_info.slot
53+
))));
54+
}
55+
};
56+
57+
let Some(slots_per_kes_period) = self.slots_per_kes_period else {
58+
return Err(Box::new(KesValidationError::Other(
59+
"Slots per KES period is not set".to_string(),
60+
)));
61+
};
62+
let Some(max_kes_evolutions) = self.max_kes_evolutions else {
63+
return Err(Box::new(KesValidationError::Other(
64+
"Max KES evolutions is not set".to_string(),
65+
)));
66+
};
67+
68+
let cert = operational_cert(&header).ok_or(Box::new(KesValidationError::Other(
69+
"Can't get operational certificate".to_string(),
70+
)))?;
71+
3872
Ok(())
3973
}
4074
}
75+
76+
fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option<OperationalCertificate<'a>> {
77+
match header {
78+
MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate {
79+
operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey,
80+
operational_cert_sequence_number: x
81+
.header_body
82+
.operational_cert
83+
.operational_cert_sequence_number,
84+
operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period,
85+
operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma,
86+
}),
87+
MultiEraHeader::ShelleyCompatible(x) => {
88+
let cert = OperationalCertificate {
89+
operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey,
90+
operational_cert_sequence_number: x.header_body.operational_cert_sequence_number,
91+
operational_cert_kes_period: x.header_body.operational_cert_kes_period,
92+
operational_cert_sigma: &x.header_body.operational_cert_sigma,
93+
};
94+
Some(cert)
95+
}
96+
_ => None,
97+
}
98+
}

0 commit comments

Comments
 (0)