From aa5a4eb971e80858b3895e0fea38cd33f2e1026c Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 7 Nov 2025 16:24:24 +0100 Subject: [PATCH 01/15] feat: make empty block_kes_validator and add KesValidationError Type --- Cargo.lock | 35 ++- Cargo.toml | 1 + common/src/validation.rs | 69 +++++- modules/block_kes_validator/Cargo.toml | 31 +++ .../src/block_kes_validator.rs | 2 + .../block_kes_validator/src/ouroboros/kes.rs | 222 ++++++++++++++++++ .../block_kes_validator/src/ouroboros/mod.rs | 1 + modules/block_kes_validator/src/state.rs | 0 processes/omnibus/Cargo.toml | 1 + processes/omnibus/omnibus.toml | 2 + 10 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 modules/block_kes_validator/Cargo.toml create mode 100644 modules/block_kes_validator/src/block_kes_validator.rs create mode 100644 modules/block_kes_validator/src/ouroboros/kes.rs create mode 100644 modules/block_kes_validator/src/ouroboros/mod.rs create mode 100644 modules/block_kes_validator/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index d6f3f452..a94eb800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,6 +113,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "acropolis_module_block_kes_validator" +version = "0.1.0" +dependencies = [ + "acropolis_common", + "anyhow", + "blake2 0.10.6", + "caryatid_sdk", + "config", + "hex", + "imbl", + "kes-summed-ed25519 0.2.1 (git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78)", + "num-traits", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "acropolis_module_block_unpacker" version = "0.2.0" @@ -498,6 +518,7 @@ dependencies = [ "acropolis_module_accounts_state", "acropolis_module_address_state", "acropolis_module_assets_state", + "acropolis_module_block_kes_validator", "acropolis_module_block_unpacker", "acropolis_module_block_vrf_validator", "acropolis_module_chain_store", @@ -3393,6 +3414,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kes-summed-ed25519" +version = "0.2.1" +source = "git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78#f69fb357d46f6a18925543d785850059569d7e78" +dependencies = [ + "blake2 0.10.6", + "ed25519-dalek", + "rand_core 0.6.4", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "lapin" version = "2.5.5" @@ -3728,7 +3761,7 @@ dependencies = [ "ed25519-dalek", "fixed", "hex", - "kes-summed-ed25519", + "kes-summed-ed25519 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "mithril-build-script", "mithril-stm", "nom 8.0.0", diff --git a/Cargo.toml b/Cargo.toml index b051845f..d826a921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "modules/chain_store", # Tracks historical information about blocks and TXs "modules/tx_submitter", # Submits TXs to peers "modules/block_vrf_validator", # Validate the VRF calculation in the block header + "modules/block_kes_validator", # Validate KES in the block header # Process builds "processes/omnibus", # All-inclusive omnibus process diff --git a/common/src/validation.rs b/common/src/validation.rs index 2e2e2bd1..72290b15 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -16,7 +16,7 @@ pub enum ValidationError { BadVRF(#[from] VrfValidationError), #[error("KES failure")] - BadKES, + BadKES(#[from] KesValidationError), #[error("Doubly spent UTXO: {0}")] DoubleSpendUTXO(String), @@ -220,3 +220,70 @@ impl PartialEq for BadVrfProofError { } } } + +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub enum KesValidationError { + /// Current KES period is before the OCert start period + #[error( + "KES Before Start OCert: OCert Start Period={}, Current Period={}", + ocert_start_period, + current_period + )] + KesBeforeStartOcert { + ocert_start_period: u64, + current_period: u64, + }, + /// Current KES period is after the valid range + #[error( + "KES After End OCert: Current Period={}, OCert Start Period={}, Max KES Evolutions={}", + current_period, + ocert_start_period, + max_kes_evolutions + )] + KesAfterEndOcert { + current_period: u64, + ocert_start_period: u64, + max_kes_evolutions: u64, + }, + /// The OCert counter is too small + #[error( + "Counter Too Small OCert: Last Counter={}, Current Counter={}", + last_counter, + current_counter + )] + CounterTooSmallOcert { + last_counter: u64, + current_counter: u64, + }, + /// The OCert counter incremented by more than 1 (Praos only) + #[error( + "Counter Over Incremented OCert: Last Counter={}, Current Counter={}", + last_counter, + current_counter + )] + CounterOverIncrementedOcert { + last_counter: u64, + current_counter: u64, + }, + /// The cold key signature on the OCert is invalid + #[error( + "Invalid Signature OCert: Counter={}, KES Period={}", + counter, + kes_period + )] + InvalidSignatureOcert { counter: u64, kes_period: u64 }, + /// The KES signature verification failed + #[error("Invalid KES Signature OCert: Current KES Period={}, KES Start Period={}, Expected Evolutions={}, Max KES Evolutions={}, Error Message={}", current_kes_period, kes_start_period, expected_evolutions, max_kes_evolutions, error_message)] + InvalidKesSignatureOcert { + current_kes_period: u64, + kes_start_period: u64, + expected_evolutions: u64, + max_kes_evolutions: u64, + error_message: String, + }, + /// No counter found for this key hash (not a stake pool or genesis delegate) + #[error("No Counter For Key Hash OCert: Pool ID={}", hex::encode(pool_id))] + NoCounterForKeyHashOcert { pool_id: PoolId }, +} diff --git a/modules/block_kes_validator/Cargo.toml b/modules/block_kes_validator/Cargo.toml new file mode 100644 index 00000000..3cca5ca0 --- /dev/null +++ b/modules/block_kes_validator/Cargo.toml @@ -0,0 +1,31 @@ +# Acropolis Block KES Validator + +[package] +name = "acropolis_module_block_kes_validator" +version = "0.1.0" +edition = "2021" +authors = ["Golddy "] +description = "Validate the KES in the block header" +license = "Apache-2.0" + +[dependencies] +acropolis_common = { path = "../../common" } + +caryatid_sdk = { workspace = true } + +anyhow = { workspace = true } +config = { workspace = true } +hex = { workspace = true } +imbl = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +blake2 = "0.10.6" +num-traits = "0.2" +thiserror = "2.0.17" + +kes-summed-ed25519 = { git = "https://github.com/txpipe/kes", rev = "f69fb357d46f6a18925543d785850059569d7e78" } + +[lib] +path = "src/block_kes_validator.rs" diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs new file mode 100644 index 00000000..0465c0e5 --- /dev/null +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -0,0 +1,2 @@ +mod ouroboros; +mod state; diff --git a/modules/block_kes_validator/src/ouroboros/kes.rs b/modules/block_kes_validator/src/ouroboros/kes.rs new file mode 100644 index 00000000..dfe76ded --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/kes.rs @@ -0,0 +1,222 @@ +use kes_summed_ed25519::{ + self as kes, + kes::{Sum6Kes, Sum6KesSig}, + traits::{KesSig, KesSk}, +}; +use std::{array::TryFromSliceError, ops::Deref}; +use thiserror::Error; + +// ------------------------------------------------------------------- SecretKey + +/// KES secret key +pub struct SecretKey<'a>(Sum6Kes<'a>); + +impl SecretKey<'_> { + /// Create a new KES secret key + pub fn from_bytes(sk_bytes: &mut Vec) -> Result, Error> { + // TODO: extend() could potentially re-allocate memory to a new location and copy the sk_bytes. + // This would leave the original memory containing the secret key without being wiped. + sk_bytes.extend([0u8; 4]); // default to period = 0 + let sum_6_kes = Sum6Kes::from_bytes(sk_bytes.as_mut_slice())?; + Ok(SecretKey(sum_6_kes)) + } + + /// Get the current period of the KES secret key + pub fn get_period(&self) -> u32 { + self.0.get_period() + } + + /// Update the KES secret key to the next period + pub fn update(&mut self) -> Result<(), Error> { + Ok(self.0.update()?) + } +} + +// ------------------------------------------------------------------- PublicKey + +/// KES public key +pub struct PublicKey(kes::PublicKey); + +impl PublicKey { + /// Size of a KES public key, in bytes; + pub const SIZE: usize = 32; +} + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Deref for PublicKey { + type Target = [u8; Self::SIZE]; + + fn deref(&self) -> &Self::Target { + self.0.as_bytes().try_into().unwrap_or_else(|e| { + unreachable!( + "Impossible! Failed to convert KES public key ({}) back to slice of known size: {e:?}", + hex::encode(self.0), + ) + }) + } +} + +impl From<&SecretKey<'_>> for PublicKey { + fn from(sk: &SecretKey<'_>) -> Self { + PublicKey(sk.0.to_pk()) + } +} + +impl From<&[u8; PublicKey::SIZE]> for PublicKey { + fn from(bytes: &[u8; PublicKey::SIZE]) -> Self { + PublicKey(kes::PublicKey::from_bytes(bytes).unwrap_or_else(|e| { + unreachable!( + "Impossible! Failed to create a KES public key from a slice ({}) of known size: {e:?}", + hex::encode(bytes) + ) + })) + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = TryFromSliceError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) + } +} + +// ------------------------------------------------------------------- Signature + +/// KES signature +pub struct Signature(Sum6KesSig); + +impl Signature { + /// Size of a KES signature, in bytes; + pub const SIZE: usize = Sum6KesSig::SIZE; + + /// Verify the KES signature + pub fn verify(&self, kes_period: u32, kes_pk: &PublicKey, msg: &[u8]) -> Result<(), Error> { + Ok(self.0.verify(kes_period, &kes_pk.0, msg)?) + } +} + +impl From<&[u8; Self::SIZE]> for Signature { + fn from(bytes: &[u8; Self::SIZE]) -> Self { + Signature(Sum6KesSig::from_bytes(bytes).unwrap_or_else(|e| { + unreachable!("Impossible! Failed to create a KES signature from a slice ({}) of known size: {e:?}", hex::encode(bytes)) + })) + } +} + +impl TryFrom<&[u8]> for Signature { + type Error = TryFromSliceError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) + } +} + +impl From<&Signature> for [u8; 448] { + fn from(sig: &Signature) -> Self { + sig.0.to_bytes() + } +} + +// ----------------------------------------------------------------------- Error + +/// KES error +#[derive(Error, Debug)] +pub enum Error { + #[error("KES error: {0}")] + Kes(#[from] kes_summed_ed25519::errors::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kes_key_evolution() { + let mut kes_sk_bytes = hex::decode( + "68b77b6e61925be0499d1445fd9210cec5bdfd5dd92662802eb2720ff70bc68fd89\ + 64580ff18bd2b232eb716dfbbeef82e2844b466ddd5dacaad9f15d3c753b3483541\ + 41e973d039b1147c48e71e5b7cadc6deb28c86e4ae4fc26e8bbe1695c3374d4eb10\ + 94a7a698722894301546466c750947778b18ac3270397efd2eced4d25ced55d2bd2\ + c09e7c0fa7b849d41787ca11defc91609d930a9870881a56a587bff20b2c5c59f63\ + ccb008be495917da3fcae536d05401b6771bb1f9356f031b3ddadbffbc426a9a23e\ + 34274b187f7e93892e990644f6273772a02d3e38bee7459ed6a9bb5760fe012e47a\ + 2e75880125e7fb072b2b7a626a5375e2039d8d748cb8ad4dd02697250d3155eee39\ + 308ecc2925405a8c15e1cbe556cc4315d43ee5101003639bcb33bd6e27da3885888\ + d7cca20b05cadbaa53941ef5282cde8f377c3bd0bf732cfac6b5d4d5597a1f72d81\ + bc0d8af634a4c760b309fe8959bbde666ff10310377b313860bd52d56fd7cb14963\ + 3beb1eb2e0076111df61e570a042f7cebae74a8de298a6f114938946230db42651e\ + a4eddf5df2d7d2f3016464073da8a9dc715817b43586a61874e576da7b47a2bb6c2\ + e19d4cbd5b1b39a24427e89b812cce6d30e0506e207f1eaab313c45a236068ea319\ + 958474237a5ffe02736e1c51c02a05999816c9253a557f09375c83acf5d7250f3bb\ + c638e10c58fb274e2002eed841ecef6a9cbc57c3157a7c3cf47e66b1741e8173b66\ + 76ac973bc9715027a3225087cabad45407b891416330485891dc9a3875488a26428\ + d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de6a3f8c321c92b77\ + 643f00d28e", + ) + .unwrap(); + + let mut kes_sk = SecretKey::from_bytes(&mut kes_sk_bytes).unwrap(); + assert_eq!( + hex::encode(PublicKey::from(&kes_sk)), + "2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292" + ); + + assert_eq!(kes_sk.get_period(), 0); + + kes_sk.update().unwrap(); + assert_eq!(kes_sk.get_period(), 1); + + kes_sk.update().unwrap(); + assert_eq!(kes_sk.get_period(), 2); + } + + #[test] + fn kes_signature_verify() { + let kes_pk_bytes = + hex::decode("2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292") + .unwrap(); + let kes_pk = PublicKey::try_from(&kes_pk_bytes[..]).unwrap(); + let kes_signature_bytes = hex::decode( + "20f1c8f9ae672e6ec75b0aa63a85e7ab7865b95f6b2907a26b54c14f49184ab52cf\ + 98ef441bb71de50380325b34f16d84fc78d137467a1b49846747cf8ee4701c56f08\ + f198b94c468d46b67b271f5bc30ab2ad14b1bdbf2be0695a00fe4b02b3060fa5212\ + 8f4cce9c5759df0ba8d71fe99456bd2e333671e45110908d03a2ec3b38599d26adf\ + 182ba63f79900fdb2732947cf8e940a4cf1e8db9b4cf4c001dbd37c60d0e38851de\ + 4910807896153be455e13161342d4c6f7bb3e4d2d35dbbbba0ebcd161be2f1ec030\ + d2f5a6059ac89dfa70dc6b3d0bc2da179c62ae95c4f9c7ad9c0387b35bf2b45b325\ + d1e0a18c0c783a0779003bf23e7a6b00cc126c5e3d51a57d41ff1707a76fb2c306a\ + 67c21473b41f1d9a7f64a670ec172a2421da03d796fa97086de8812304f4f96bd45\ + 243d0a2ad6c48a69d9e2c0afbb1333acee607d18eb3a33818c3c9d5bb72cade8893\ + 79008bf60d436298cb0cfc6159332cb1af1de4f1d64e79c399d058ac4993704eed6\ + 7917093f89db6cde830383e69aa400ba3225087cabad45407b891416330485891dc\ + 9a3875488a26428d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de\ + 6a3f8c321c92b77643f00d28e", + ) + .unwrap(); + let kes_signature = Signature::try_from(&kes_signature_bytes[..]).unwrap(); + let kes_period = 36u32; + let kes_msg = hex::decode( + "8a1a00a50f121a0802d24458203deea82abe788d260b8987a522aadec86c9f098e8\ + 8a57d7cfcdb24f474a7afb65820cad3c900ca6baee9e65bf61073d900bfbca458ee\ + ca6d0b9f9931f5b1017a8cd65820576d49e98adfab65623dc16f9fff2edd210e8dd\ + 1d4588bfaf8af250beda9d3c7825840d944b8c81000fc1182ec02194ca9eca510fd\ + 84995d22bfe1842190b39d468e5ecbd863969e0c717b0071a371f748d44c895fa92\ + 33094cefcd3107410baabb19a5850f2a29f985d37ca8eb671c2847fab9cc45c9373\ + 8a430b4e43837e7f33028b190a7e55152b0e901548961a66d56eebe72d616f9e68f\ + d13e9955ccd8611c201a5b422ac8ef56af74cb657b5b868ce9d850f1945d1582063\ + 9d4986d17de3cac8079a3b25d671f339467aa3a9948e29992dafebf90f719f84582\ + 02e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b29217\ + 1903e958401feeeabc7460b19370f4050e986b558b149fdc8724b4a4805af8fe45c\ + 8e7a7c6753894ad7a1b9c313da269ddc5922e150da3b378977f1dfea79fc52fd2c1\ + 2f08820901", + ) + .unwrap(); + assert!(kes_signature.verify(kes_period, &kes_pk, &kes_msg).is_ok()); + } +} diff --git a/modules/block_kes_validator/src/ouroboros/mod.rs b/modules/block_kes_validator/src/ouroboros/mod.rs new file mode 100644 index 00000000..b081d509 --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/mod.rs @@ -0,0 +1 @@ +mod kes; diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs new file mode 100644 index 00000000..e69de29b diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 93e991f9..4db6bebd 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -31,6 +31,7 @@ acropolis_module_address_state = { path = "../../modules/address_state" } acropolis_module_consensus = { path = "../../modules/consensus" } acropolis_module_historical_accounts_state = { path = "../../modules/historical_accounts_state" } acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } +acropolis_module_block_kes_validator = { path = "../../modules/block_kes_validator" } caryatid_process = { workspace = true } caryatid_sdk = { workspace = true } diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 7de217e1..13173f8e 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -139,6 +139,8 @@ store-transactions = false [module.block-vrf-validator] +[module.block-kes-validator] + [module.clock] [module.rest-server] From 95904957f72a46b34f5cf5d29bd5913d270d6739 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 10 Nov 2025 15:32:48 +0100 Subject: [PATCH 02/15] wip: add kes validation module --- common/src/validation.rs | 47 +++-- .../src/block_kes_validator.rs | 190 +++++++++++++++++- .../src/kes_validation_publisher.rs | 52 +++++ modules/block_kes_validator/src/state.rs | 40 ++++ 4 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 modules/block_kes_validator/src/kes_validation_publisher.rs diff --git a/common/src/validation.rs b/common/src/validation.rs index 8b1ba406..2cd674a8 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -229,7 +229,16 @@ impl PartialEq for BadVrfProofError { /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum KesValidationError { - /// Current KES period is before the OCert start period + #[error("{0}")] + KesSignatureError(#[from] KesSignatureError), + #[error("{0}")] + OperationalCertificateError(#[from] OperationalCertificateError), +} + +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub enum KesSignatureError { + /// **Cause:** Current KES period is before the operational certificate's + /// start period. #[error( "KES Before Start OCert: OCert Start Period={}, Current Period={}", ocert_start_period, @@ -239,7 +248,8 @@ pub enum KesValidationError { ocert_start_period: u64, current_period: u64, }, - /// Current KES period is after the valid range + /// **Cause:** Current KES period exceeds the operational certificate's + /// validity period. #[error( "KES After End OCert: Current Period={}, OCert Start Period={}, Max KES Evolutions={}", current_period, @@ -251,7 +261,21 @@ pub enum KesValidationError { ocert_start_period: u64, max_kes_evolutions: u64, }, - /// The OCert counter is too small + /// **Cause:** The KES signature on the block header is cryptographically invalid. + #[error("Invalid KES Signature OCert: Current KES Period={}, KES Start Period={}, Expected Evolutions={}, Max KES Evolutions={}, Error Message={}", current_kes_period, kes_start_period, expected_evolutions, max_kes_evolutions, error_message)] + InvalidKesSignatureOcert { + current_kes_period: u64, + kes_start_period: u64, + expected_evolutions: u64, + max_kes_evolutions: u64, + error_message: String, + }, +} + +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub enum OperationalCertificateError { + /// **Cause:** The operational certificate counter in the header is not greater + /// than the last counter used by this pool. #[error( "Counter Too Small OCert: Last Counter={}, Current Counter={}", last_counter, @@ -261,7 +285,8 @@ pub enum KesValidationError { last_counter: u64, current_counter: u64, }, - /// The OCert counter incremented by more than 1 (Praos only) + /// **Cause:** OCert counter jumped by more than 1. While not strictly invalid, + /// this is suspicious and may indicate key compromise. (Praos Only) #[error( "Counter Over Incremented OCert: Last Counter={}, Current Counter={}", last_counter, @@ -271,23 +296,15 @@ pub enum KesValidationError { last_counter: u64, current_counter: u64, }, - /// The cold key signature on the OCert is invalid + /// **Cause:** The cold key signature on the operational certificate is invalid. + /// The OCert was not properly signed by the pool's cold key. #[error( "Invalid Signature OCert: Counter={}, KES Period={}", counter, kes_period )] InvalidSignatureOcert { counter: u64, kes_period: u64 }, - /// The KES signature verification failed - #[error("Invalid KES Signature OCert: Current KES Period={}, KES Start Period={}, Expected Evolutions={}, Max KES Evolutions={}, Error Message={}", current_kes_period, kes_start_period, expected_evolutions, max_kes_evolutions, error_message)] - InvalidKesSignatureOcert { - current_kes_period: u64, - kes_start_period: u64, - expected_evolutions: u64, - max_kes_evolutions: u64, - error_message: String, - }, - /// No counter found for this key hash (not a stake pool or genesis delegate) + /// **Cause:** No counter found for this key hash (not a stake pool or genesis delegate) #[error("No Counter For Key Hash OCert: Pool ID={}", hex::encode(pool_id))] NoCounterForKeyHashOcert { pool_id: PoolId }, } diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs index 0465c0e5..9094de0e 100644 --- a/modules/block_kes_validator/src/block_kes_validator.rs +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -1,2 +1,190 @@ -mod ouroboros; +//! Acropolis Block KES Validator module for Caryatid +//! Validate KES signatures in the block header + +use acropolis_common::{ + messages::{CardanoMessage, Message}, + state_history::{StateHistory, StateHistoryStore}, + BlockInfo, BlockStatus, +}; +use anyhow::Result; +use caryatid_sdk::{module, Context, Module, Subscription}; +use config::Config; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{error, info, info_span, Instrument}; mod state; +use state::State; + +use crate::kes_validation_publisher::KesValidationPublisher; +mod kes_validation_publisher; +mod ouroboros; + +const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = + ("validation-kes-publisher-topic", "cardano.validation.kes"); + +const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( + "bootstrapped-subscribe-topic", + "cardano.sequence.bootstrapped", +); +const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( + "protocol-parameters-subscribe-topic", + "cardano.protocol.parameters", +); +const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = + ("blocks-subscribe-topic", "cardano.block.proposed"); + +/// Block KES Validator module +#[module( + message_type(Message), + name = "block-kes-validator", + description = "Validate the KES signatures in the block header" +)] + +pub struct BlockKesValidator; + +impl BlockKesValidator { + #[allow(clippy::too_many_arguments)] + async fn run( + history: Arc>>, + mut kes_validation_publisher: KesValidationPublisher, + mut bootstrapped_subscription: Box>, + mut blocks_subscription: Box>, + mut protocol_parameters_subscription: Box>, + ) -> Result<()> { + let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; + let genesis = match bootstrapped_message.as_ref() { + Message::Cardano((_, CardanoMessage::GenesisComplete(complete))) => { + complete.values.clone() + } + _ => panic!("Unexpected message in genesis completion topic: {bootstrapped_message:?}"), + }; + + // Consume initial protocol parameters + let _ = protocol_parameters_subscription.read().await?; + + loop { + // Get a mutable state + let mut state = history.lock().await.get_or_init_with(State::new); + let mut current_block: Option = None; + + let (_, message) = blocks_subscription.read().await?; + match message.as_ref() { + Message::Cardano((block_info, CardanoMessage::BlockAvailable(block_msg))) => { + // handle rollback here + if block_info.status == BlockStatus::RolledBack { + state = history.lock().await.get_rolled_back_state(block_info.number); + } + current_block = Some(block_info.clone()); + let is_new_epoch = block_info.new_epoch && block_info.epoch > 0; + + if is_new_epoch { + // read epoch boundary messages + let protocol_parameters_message_f = protocol_parameters_subscription.read(); + + let (_, protocol_parameters_msg) = protocol_parameters_message_f.await?; + let span = info_span!( + "block_kes_validator.handle_protocol_parameters", + epoch = block_info.epoch + ); + span.in_scope(|| match protocol_parameters_msg.as_ref() { + Message::Cardano((block_info, CardanoMessage::ProtocolParams(msg))) => { + Self::check_sync(¤t_block, block_info); + state.handle_protocol_parameters(msg); + } + _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), + }); + } + + let span = + info_span!("block_kes_validator.validate", block = block_info.number); + async { + let result = state + .validate_block_kes(block_info, &block_msg.header, &genesis) + .map_err(|e| *e); + if let Err(e) = kes_validation_publisher + .publish_kes_validation(block_info, result) + .await + { + error!("Failed to publish KES validation: {e}") + } + } + .instrument(span) + .await; + } + _ => error!("Unexpected message type: {message:?}"), + } + + // Commit the new state + if let Some(block_info) = current_block { + history.lock().await.commit(block_info.number, state); + } + } + } + + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Publish topics + let validation_kes_publisher_topic = config + .get_string(DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC.0) + .unwrap_or(DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC.1.to_string()); + info!("Creating validation KES publisher on '{validation_kes_publisher_topic}'"); + + // Subscribe topics + let bootstrapped_subscribe_topic = config + .get_string(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating subscriber for bootstrapped on '{bootstrapped_subscribe_topic}'"); + let protocol_parameters_subscribe_topic = config + .get_string(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating subscriber for protocol parameters on '{protocol_parameters_subscribe_topic}'"); + + let blocks_subscribe_topic = config + .get_string(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating blocks subscription on '{blocks_subscribe_topic}'"); + + // publishers + let kes_validation_publisher = + KesValidationPublisher::new(context.clone(), validation_kes_publisher_topic); + + // Subscribers + let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; + let protocol_parameters_subscription = + context.subscribe(&protocol_parameters_subscribe_topic).await?; + let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; + + // state history + let history = Arc::new(Mutex::new(StateHistory::::new( + "block_kes_validator", + StateHistoryStore::default_block_store(), + ))); + + // Start run task + context.run(async move { + Self::run( + history, + kes_validation_publisher, + bootstrapped_subscription, + blocks_subscription, + protocol_parameters_subscription, + ) + .await + .unwrap_or_else(|e| error!("Failed: {e}")); + }); + + Ok(()) + } + + /// Check for synchronisation + fn check_sync(expected: &Option, actual: &BlockInfo) { + if let Some(ref block) = expected { + if block.number != actual.number { + error!( + expected = block.number, + actual = actual.number, + "Messages out of sync" + ); + } + } + } +} diff --git a/modules/block_kes_validator/src/kes_validation_publisher.rs b/modules/block_kes_validator/src/kes_validation_publisher.rs new file mode 100644 index 00000000..2b92c53e --- /dev/null +++ b/modules/block_kes_validator/src/kes_validation_publisher.rs @@ -0,0 +1,52 @@ +use acropolis_common::{ + messages::{CardanoMessage, Message}, + validation::{KesValidationError, ValidationError, ValidationStatus}, + BlockInfo, +}; +use caryatid_sdk::Context; +use std::sync::Arc; +use tracing::error; + +/// Message publisher for Block header KES Validation Result +pub struct KesValidationPublisher { + /// Module context + context: Arc>, + + /// Topic to publish on + topic: String, +} + +impl KesValidationPublisher { + /// Construct with context and topic to publish on + pub fn new(context: Arc>, topic: String) -> Self { + Self { context, topic } + } + + pub async fn publish_kes_validation( + &mut self, + block: &BlockInfo, + validation_result: Result<(), KesValidationError>, + ) -> anyhow::Result<()> { + let validation_status = match validation_result { + Ok(_) => ValidationStatus::Go, + Err(error) => { + error!( + "KES validation failed: {} of block {}", + error.clone(), + block.number + ); + ValidationStatus::NoGo(ValidationError::from(error)) + } + }; + self.context + .message_bus + .publish( + &self.topic, + Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::BlockValidation(validation_status), + ))), + ) + .await + } +} diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index e69de29b..e7f1d1ec 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -0,0 +1,40 @@ +use acropolis_common::{ + genesis_values::GenesisValues, messages::ProtocolParamsMessage, validation::KesValidationError, + BlockInfo, PoolId, +}; +use imbl::HashMap; + +#[derive(Default, Debug, Clone)] +pub struct State { + pub ocert_counters: HashMap, + + pub slots_per_kes_period: Option, + + pub max_kes_evolutions: Option, +} + +impl State { + pub fn new() -> Self { + Self { + ocert_counters: HashMap::new(), + slots_per_kes_period: None, + max_kes_evolutions: None, + } + } + + pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { + if let Some(shelley_params) = msg.params.shelley.as_ref() { + self.slots_per_kes_period = Some(shelley_params.slots_per_kes_period as u64); + self.max_kes_evolutions = Some(shelley_params.max_kes_evolutions as u64); + } + } + + pub fn validate_block_kes( + &self, + block_info: &BlockInfo, + raw_header: &[u8], + genesis: &GenesisValues, + ) -> Result<(), Box> { + Ok(()) + } +} From 19e960eaa4658fb2330e34e011e5ba51a4ca3b28 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 11 Nov 2025 05:51:00 +0100 Subject: [PATCH 03/15] feat: add kes ocert validation --- Cargo.lock | 3 +- common/src/validation.rs | 41 ++++++------- modules/block_kes_validator/Cargo.toml | 3 +- .../block_kes_validator/src/ouroboros/mod.rs | 3 +- .../src/ouroboros/praos.rs | 56 ++++++++++++++++++ modules/block_kes_validator/src/state.rs | 58 +++++++++++++++++++ 6 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 modules/block_kes_validator/src/ouroboros/praos.rs diff --git a/Cargo.lock b/Cargo.lock index a94eb800..9b666977 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,13 +119,12 @@ version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", - "blake2 0.10.6", "caryatid_sdk", "config", "hex", "imbl", "kes-summed-ed25519 0.2.1 (git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78)", - "num-traits", + "pallas 0.33.0", "serde", "serde_json", "thiserror 2.0.17", diff --git a/common/src/validation.rs b/common/src/validation.rs index 2cd674a8..3deb16cc 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -229,10 +229,12 @@ impl PartialEq for BadVrfProofError { /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum KesValidationError { - #[error("{0}")] + #[error("KES Signature Error: {0}")] KesSignatureError(#[from] KesSignatureError), - #[error("{0}")] + #[error("Operational Certificate Error: {0}")] OperationalCertificateError(#[from] OperationalCertificateError), + #[error("Other Kes Validation Error: {0}")] + Other(String), } #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] @@ -274,36 +276,35 @@ pub enum KesSignatureError { #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum OperationalCertificateError { + /// **Cause:** The operational certificate is malformed. + #[error("Malformed Signature OCert: Reason={}", reason)] + MalformedSignatureOcert { reason: String }, + /// **Cause:** The cold key signature on the operational certificate is invalid. + /// The OCert was not properly signed by the pool's cold key. + #[error("Invalid Signature OCert: Issuer={}", hex::encode(issuer))] + InvalidSignatureOcert { issuer: Vec }, /// **Cause:** The operational certificate counter in the header is not greater /// than the last counter used by this pool. #[error( - "Counter Too Small OCert: Last Counter={}, Current Counter={}", - last_counter, - current_counter + "Counter Too Small OCert: Latest Counter={}, Declared Counter={}", + latest_counter, + declared_counter )] CounterTooSmallOcert { - last_counter: u64, - current_counter: u64, + latest_counter: u64, + declared_counter: u64, }, /// **Cause:** OCert counter jumped by more than 1. While not strictly invalid, /// this is suspicious and may indicate key compromise. (Praos Only) #[error( - "Counter Over Incremented OCert: Last Counter={}, Current Counter={}", - last_counter, - current_counter + "Counter Over Incremented OCert: Latest Counter={}, Declared Counter={}", + latest_counter, + declared_counter )] CounterOverIncrementedOcert { - last_counter: u64, - current_counter: u64, + latest_counter: u64, + declared_counter: u64, }, - /// **Cause:** The cold key signature on the operational certificate is invalid. - /// The OCert was not properly signed by the pool's cold key. - #[error( - "Invalid Signature OCert: Counter={}, KES Period={}", - counter, - kes_period - )] - InvalidSignatureOcert { counter: u64, kes_period: u64 }, /// **Cause:** No counter found for this key hash (not a stake pool or genesis delegate) #[error("No Counter For Key Hash OCert: Pool ID={}", hex::encode(pool_id))] NoCounterForKeyHashOcert { pool_id: PoolId }, diff --git a/modules/block_kes_validator/Cargo.toml b/modules/block_kes_validator/Cargo.toml index 3cca5ca0..db028af0 100644 --- a/modules/block_kes_validator/Cargo.toml +++ b/modules/block_kes_validator/Cargo.toml @@ -21,9 +21,8 @@ tokio = { workspace = true } tracing = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } -blake2 = "0.10.6" -num-traits = "0.2" thiserror = "2.0.17" +pallas = { workspace = true } kes-summed-ed25519 = { git = "https://github.com/txpipe/kes", rev = "f69fb357d46f6a18925543d785850059569d7e78" } diff --git a/modules/block_kes_validator/src/ouroboros/mod.rs b/modules/block_kes_validator/src/ouroboros/mod.rs index b081d509..a72745d0 100644 --- a/modules/block_kes_validator/src/ouroboros/mod.rs +++ b/modules/block_kes_validator/src/ouroboros/mod.rs @@ -1 +1,2 @@ -mod kes; +pub mod kes; +pub mod praos; diff --git a/modules/block_kes_validator/src/ouroboros/praos.rs b/modules/block_kes_validator/src/ouroboros/praos.rs new file mode 100644 index 00000000..5d0cbcca --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/praos.rs @@ -0,0 +1,56 @@ +use acropolis_common::validation::OperationalCertificateError; +use pallas::crypto::key::ed25519; + +pub struct OperationalCertificate<'a> { + pub operational_cert_hot_vkey: &'a [u8], + pub operational_cert_sequence_number: u64, + pub operational_cert_kes_period: u64, + pub operational_cert_sigma: &'a [u8], +} + +pub fn validate_operational_certificate<'a>( + certificate: OperationalCertificate<'a>, + issuer: &ed25519::PublicKey, + latest_sequence_number: u64, + is_praos: bool, +) -> Result<(), OperationalCertificateError> { + // Verify the Operational Certificate signature + let signature = + ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| { + OperationalCertificateError::MalformedSignatureOcert { + reason: error.to_string(), + } + })?; + + let declared_sequence_number = certificate.operational_cert_sequence_number; + + // Check the sequence number of the operational certificate. It should either be the same + // as the latest known sequence number for the issuer or one greater. + if declared_sequence_number < latest_sequence_number { + return Err(OperationalCertificateError::CounterTooSmallOcert { + latest_counter: latest_sequence_number, + declared_counter: declared_sequence_number, + }); + } + + // this is only for praos protocol + if is_praos && (declared_sequence_number - latest_sequence_number) > 1 { + return Err(OperationalCertificateError::CounterOverIncrementedOcert { + latest_counter: latest_sequence_number, + declared_counter: declared_sequence_number, + }); + } + + // The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period + let mut message = Vec::new(); + message.extend_from_slice(certificate.operational_cert_hot_vkey); + message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes()); + message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes()); + if !issuer.verify(&message, &signature) { + return Err(OperationalCertificateError::InvalidSignatureOcert { + issuer: issuer.as_ref().to_vec(), + }); + } + + Ok(()) +} diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index e7f1d1ec..57f11fce 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -1,8 +1,11 @@ +use crate::ouroboros::praos::OperationalCertificate; use acropolis_common::{ genesis_values::GenesisValues, messages::ProtocolParamsMessage, validation::KesValidationError, BlockInfo, PoolId, }; use imbl::HashMap; +use pallas::ledger::{primitives::babbage::OperationalCert, traverse::MultiEraHeader}; +use tracing::error; #[derive(Default, Debug, Clone)] pub struct State { @@ -35,6 +38,61 @@ impl State { raw_header: &[u8], genesis: &GenesisValues, ) -> Result<(), Box> { + // Validation starts after Shelley Era + if block_info.epoch < genesis.shelley_epoch { + return Ok(()); + } + + let header = match MultiEraHeader::decode(block_info.era as u8, None, raw_header) { + Ok(header) => header, + Err(e) => { + error!("Can't decode header {}: {e}", block_info.slot); + return Err(Box::new(KesValidationError::Other(format!( + "Can't decode header {}: {e}", + block_info.slot + )))); + } + }; + + let Some(slots_per_kes_period) = self.slots_per_kes_period else { + return Err(Box::new(KesValidationError::Other( + "Slots per KES period is not set".to_string(), + ))); + }; + let Some(max_kes_evolutions) = self.max_kes_evolutions else { + return Err(Box::new(KesValidationError::Other( + "Max KES evolutions is not set".to_string(), + ))); + }; + + let cert = operational_cert(&header).ok_or(Box::new(KesValidationError::Other( + "Can't get operational certificate".to_string(), + )))?; + Ok(()) } } + +fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option> { + match header { + MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate { + operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey, + operational_cert_sequence_number: x + .header_body + .operational_cert + .operational_cert_sequence_number, + operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period, + operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma, + }), + MultiEraHeader::ShelleyCompatible(x) => { + let cert = OperationalCertificate { + operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey, + operational_cert_sequence_number: x.header_body.operational_cert_sequence_number, + operational_cert_kes_period: x.header_body.operational_cert_kes_period, + operational_cert_sigma: &x.header_body.operational_cert_sigma, + }; + Some(cert) + } + _ => None, + } +} From 1fb00b8862cbbd46686c83359663583c923c1f43 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 11 Nov 2025 16:43:58 +0100 Subject: [PATCH 04/15] feat: add kes signature check function and test data --- .cargo/audit.toml | 1 - common/src/validation.rs | 20 +++++++--- .../src/ouroboros/data/4490511.cbor | 1 + .../src/ouroboros/data/4556956.cbor | 1 + .../src/ouroboros/data/4576496.cbor | 1 + .../src/ouroboros/data/7854823.cbor | 1 + .../src/ouroboros/praos.rs | 40 ++++++++++++++++++- 7 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 modules/block_kes_validator/src/ouroboros/data/4490511.cbor create mode 100644 modules/block_kes_validator/src/ouroboros/data/4556956.cbor create mode 100644 modules/block_kes_validator/src/ouroboros/data/4576496.cbor create mode 100644 modules/block_kes_validator/src/ouroboros/data/7854823.cbor diff --git a/.cargo/audit.toml b/.cargo/audit.toml index f1618d62..85a6c724 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,3 +1,2 @@ [advisories] -ignore = [] informational_warnings = [] \ No newline at end of file diff --git a/common/src/validation.rs b/common/src/validation.rs index 3deb16cc..60bc15e0 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -229,10 +229,15 @@ impl PartialEq for BadVrfProofError { /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum KesValidationError { + /// **Cause:** The KES signature on the block header is invalid. #[error("KES Signature Error: {0}")] KesSignatureError(#[from] KesSignatureError), + /// **Cause:** The operational certificate is invalid. #[error("Operational Certificate Error: {0}")] OperationalCertificateError(#[from] OperationalCertificateError), + /// **Cause:** Some data has incorrect bytes + #[error("TryFromSlice: {0}")] + TryFromSlice(String), #[error("Other Kes Validation Error: {0}")] Other(String), } @@ -264,13 +269,16 @@ pub enum KesSignatureError { max_kes_evolutions: u64, }, /// **Cause:** The KES signature on the block header is cryptographically invalid. - #[error("Invalid KES Signature OCert: Current KES Period={}, KES Start Period={}, Expected Evolutions={}, Max KES Evolutions={}, Error Message={}", current_kes_period, kes_start_period, expected_evolutions, max_kes_evolutions, error_message)] + #[error( + "Invalid KES Signature OCert: Current Period={}, OCert Start Period={}, Reason={}", + current_period, + ocert_start_period, + reason + )] InvalidKesSignatureOcert { - current_kes_period: u64, - kes_start_period: u64, - expected_evolutions: u64, - max_kes_evolutions: u64, - error_message: String, + current_period: u64, + ocert_start_period: u64, + reason: String, }, } diff --git a/modules/block_kes_validator/src/ouroboros/data/4490511.cbor b/modules/block_kes_validator/src/ouroboros/data/4490511.cbor new file mode 100644 index 00000000..3c5ac69a --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/data/4490511.cbor @@ -0,0 +1 @@ +828f1a0044850f1a00448e005820f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a45758209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda58206d930cc9d1baade5cd1c70fbc025d3377ce946760c48e511d1abdf8acff6ff1c82584036ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab7365850405aa370ff009544a2be4aa5bc52c4456333f4f9b6571d66c5590d2e5629d08b3a3609f4bd0502ed5d0be1abdb7f2ab76aaeae47fe111b0335a4e4def64693162794b8d3c1ca71500f16b1e244724c03825840da5ccc9f8fd62f6c290b5bb2ed9a4258fc9481dd8a0ac80f8936702ad7709a87814d14dca02ce22d7a3e150d171e57914cc058081941e3c6737987524076b6935850402fcbd1d50e4b42fa81dce2eada18df1803af49273d05cbe4a1870983d86759a61005d5d942a53fb68389fe119b5dae823fa3cd6668dc257556bc52086fd879b082dace60edcf0cd592f63103bcd10e0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff75820ae981f4a58d135f98d0a0c5aab9fc04944c8409f65187f9778b57905e43769570000584008d56fe9c28eeeb99bda8920a9887f468f1d74bb03c37ef3caffffbee68d88eb33341498ac145a2422c77546db61c2da782587cdf934d69b113ce52ab8bd8b0b02005901c03de52bc307718194c07824cdc0d2fa016e60b19bc1c7162978fdac55cf43d2ac7f3aea635ffc98d53570a98ac19c703dfda935aac478d84f0281f7c8bc7a180b6aa7e738e9381ebb86baf842af4ee229a3e3908cfec16153f1dd5e21dba0316e7625e4c2a8db495d80ca3c93862fcf4ad0a8a79a20f7d3d9733f9dccbb8ac6d623bbec2bb6dbf170a3c33d32a15d2cc02b3ce78c8d61af768fa82067254b2fe3bd7a1e5cdacf71b50998eec98a48b6970eacdc74d5ae40224f95850fbd88df96e192767c641a70ebf2bb9dc7d32a1e36d1113c09f5591622b35aed5c06bf7b894b882c56bd8e12b7da5b16a341e59d3239a3d14f9d0a5ac006dfdc258e215eacb563954ea234797754b9276f9746d1d6589e10f0dfda8130d422db636d02dec7c8b45ff97de40ac541421e3f2d1aed4246cc5bb54600fa567275046a979f3b4d067c0869732129234df68f1150b09285ecf71385034c79ace9e74186bd811770fcd43a449aa9466165731f92ae73ba4ef9c665a1f8851420a8b1568b711fabca735f474a6a800751f73fbbb3ed2ace0c8bdb2c14eed4032b085dd0ac8f6b0d984a441d337ffe6903aa794728a56360e106c84fc1b943af0e383fd1f1939a2416 \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/data/4556956.cbor b/modules/block_kes_validator/src/ouroboros/data/4556956.cbor new file mode 100644 index 00000000..68eb2b74 --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/data/4556956.cbor @@ -0,0 +1 @@ +828f1a0045889c1a0058e1515820d5852411975dc3c5da5042f48372aa533b96c6d24017f9291cf65e730b5835555820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f6108258403962c816bc3989fe2b817e3a8f8deb54f6b0cf69105c7b041b3f1e90dfbf05a96c8912e11046647053d8e0c43fddf984082231946d3250a4182659e9b18cdd39585084e17ca6179f8d416f9bdc11f39714cb546ba0bc2274cf9d2c560a83d76942a31c55f30e95b310a9db9726c81cf37913f1a687dee0f21b5c553d863d393a4b1128425bb11901a6dc0a5bcd5d2eae82028258400018db71ca133eaa423d45ebfc33b6dbe2d35296f89e4ca3fe09810d8805255414ad3b5c97d0c3c7eca86d8d4c07035aa479bd6af9d9e986b8c4f9a5f76c9382585024df0024d93acdc1eb8b2a3a7b0f348fba765a4f27590eb6420458b34507ee655089866644bee297644c63fe8bc2aa3574a6b6a76e34fcaa5ce4010dd2648fb7118fbc7b6899024fd6369c43155c450f0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff7582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0f04b785404897ff6e361e76bc9a5f62c5b1af03e255a6110b09fc3be9d50ba1f2314076c09320a9bc9b755b6f9483325f9dbdab108bd8e412a63b1e15e54d60d17a33403716c4d5a57e851b90f21569cfdf4b7cd74ceb725d9173714d8ea7dda528f6848b0001f921382cdd64ae1ba60d7f725af64f10c572ed56ae28e187e4fe11c45ea269b508bcb6c08748be32e9bcf27df9c8853dc7272a278e60120f7fbe927d8614bbc3a1d9bab4a00199313f2f81f6b62a2f35496c04c1d7f8dae984695270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/data/4576496.cbor b/modules/block_kes_validator/src/ouroboros/data/4576496.cbor new file mode 100644 index 00000000..c58d4f5f --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/data/4576496.cbor @@ -0,0 +1 @@ +828f1a0045d4f01a005eebcd5820d0547673599cc31048011657b1ac2ae14ff15b2103702b4071fd0c0e1caaeecf5820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f610825840ce902041f973eb263968e1de7f6255de7a9ddcafa343c2d2ed89c4daaca121d87621f683634ef07ff8234f69e381bd593e6229e92b2416fb463e5b50f1df675a5850b6568f768f3e4ca83dee729f934b83438fd693945f6c08110c1d9963024c904c1f836b51792a839aaedeb9381f65273abee234ea9e3205810e08ec03413244e2345aa58992d60e30aa38c3191f11db03825840000f783edf754904b325197136c4cdf5badf6a32ee7b86758d005aed320eb298efcd137da7507a51a7c580211017b14b08267c947b179358ebd5ee77d6b473b4585051632cef6eb7a950d94086d8f8770028d32ff55dc39741f7d45e061fd558383d5e18eb15aa6192a9c0bd16a5dbfcbee0dc041d158a2c50e67e80d3fc4a78dab889a5b703ab815afece18d00ed0431f0019018c582040fd0376795eec5777148337d4a5d37b72c358c30da034de8bac5e23c4525407582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0c28b49147a742ee1a357f459315234446df337e2933df741307268fc064c32c0d465e2785f29ac9a79bfe55db85be4f3e36bfc2b7e15f76db91c46cbc2d27601b08c6020c1aee33d51ab7a66f9432d61d1dbb5bc3a7f6b183d414b94ffacbb459600a4ef2b27ece3c5682b50d1316dff928d7e627cb4f6754321e6465acb16cd360c41571150cd17dafdb642a27bbd5e80ce24a30ac2126a5c27fd866f7e43ab6c446de810e15924cf0b05873c6f4e49e518c139333d6d47c1ae709771ac5abf95270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/data/7854823.cbor b/modules/block_kes_validator/src/ouroboros/data/7854823.cbor new file mode 100644 index 00000000..1199b9b6 --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/data/7854823.cbor @@ -0,0 +1 @@ +828a1a0077dae71a046344c15820991e0770751806103894d8fffaf86fcc90f238c6ac00bdc7cb0a953e4a59fd93582041ec779d32f5ca1fc81a8cb4152e6bc1f0ab569abe1c0de8b66d76242d23a70f5820b5b6a6b790e6926f6ba856255aaf3eb5192cc80c03eeec7469cd3f64344bafb78258403f42ef44b8525ee45ca45a094191f00b359e9205a400b0607c87fe488f3b44442afc9e7c3e28ded9080c71d84eb7840ecb2e7240721ee243b45a761048ad6c53585054ac3b2bc94b54705b951146a83cba92041fdf537644628c64e9549365e74c10502fb7c23f34d7daa8327843f728513b225044a9390f86fb2a83951c4a39a13c17472c71cf50d74f1ce12d04dc85e9021a00015b1158202c1657b98fabe337cff8811a0b198dd36dfa61a415fdc5e5759f43cfa1103c48845820249caec7a56b2ede7dfdb956178fc6d7d5c96b5f35e63ddc7987e9261ac277c70b1902065840c4bbbda856bebac009d32c276080a54bb6322ab3252d09ad4c7c4a3e3f9b8931dd81b1795dbd165d8d3ca493d77966eb682bfff0edc1d151ef0ad42efb91ba0d8207005901c03824c9afe0176e3abf9e9bcf6972fb8c408d218fe0eab8026780a9995c7cc8fbf2a6f7d54fcb138bcff109b578d020baf5dbae751dad81980d52146afc615905ea103db0ff10d488fc9d0dbfa410066768d93c566fc346ff4b81bd41137df20c446771dd0d24399d61a9caeddce7abb55e6647247158ac252cf87e56871f4535857f6dbbf90b99d9b4bd6ca20705f7deff4e7d3220c002c4e7e5d96927751d96e08c92886ac223a4f7141625da97819a3b574dbf80ca50418f33ce2858b41f2fe63290bda8fcfe8382defc7ce721ed6d96466efe69a0bcb31a3951447b3e73c07e167240a99b492a71b0f9a6af321620d16b944b7b7ce89353ce0b65fc19f3744357603fca23f2b0056c607f817b24ee15b51008ab43e470016e46c0a1589bf184b46b79e808750f7a96fa747387414b9e691b95e7003e825308e4cd4fcf67d87233cb90282e3a5e65ce5cd3448e2a5d5ee6b232a431a45c252ed9a04dbee9cf89c5d5f975481b3be2623bf4a517205b69516d59e0a1877643e25485846911252315499cc705061ee74bd8a6ca2a9ffe46f2905839372b008074d775b6f6f7bb3f245263d306f0194133f947909ff4af9f9815800f1173ef7ea602886f55e1fd \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/praos.rs b/modules/block_kes_validator/src/ouroboros/praos.rs index 5d0cbcca..72bef0ba 100644 --- a/modules/block_kes_validator/src/ouroboros/praos.rs +++ b/modules/block_kes_validator/src/ouroboros/praos.rs @@ -1,6 +1,8 @@ -use acropolis_common::validation::OperationalCertificateError; +use acropolis_common::validation::{KesSignatureError, OperationalCertificateError}; use pallas::crypto::key::ed25519; +use crate::ouroboros::kes; + pub struct OperationalCertificate<'a> { pub operational_cert_hot_vkey: &'a [u8], pub operational_cert_sequence_number: u64, @@ -8,6 +10,42 @@ pub struct OperationalCertificate<'a> { pub operational_cert_sigma: &'a [u8], } +pub fn validate_kes_signature<'a>( + slot_kes_period: u64, + opcert_kes_period: u64, + header_body: &[u8], + public_key: &kes::PublicKey, + signature: &kes::Signature, + max_kes_evolutions: u64, +) -> Result<(), KesSignatureError> { + if opcert_kes_period > slot_kes_period { + return Err(KesSignatureError::KesBeforeStartOcert { + ocert_start_period: opcert_kes_period, + current_period: slot_kes_period, + }); + } + + if slot_kes_period >= opcert_kes_period + max_kes_evolutions { + return Err(KesSignatureError::KesAfterEndOcert { + current_period: slot_kes_period, + ocert_start_period: opcert_kes_period, + max_kes_evolutions, + }); + } + + let kes_period = (slot_kes_period - opcert_kes_period) as u32; + + signature.verify(kes_period, public_key, header_body).map_err(|error| { + KesSignatureError::InvalidKesSignatureOcert { + current_period: slot_kes_period, + ocert_start_period: opcert_kes_period, + reason: error.to_string(), + } + })?; + + Ok(()) +} + pub fn validate_operational_certificate<'a>( certificate: OperationalCertificate<'a>, issuer: &ed25519::PublicKey, From f69be53d2cf936e17793e5d8445a2b6e6423d183 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 13:55:31 +0100 Subject: [PATCH 05/15] feat: implement kes validation with genesis key's block test --- common/src/validation.rs | 6 + .../src/ouroboros/kes_validation.rs | 231 ++++++++++++++++++ .../block_kes_validator/src/ouroboros/mod.rs | 2 + .../src/ouroboros/praos.rs | 106 +------- .../src/ouroboros/tpraos.rs | 15 ++ modules/block_kes_validator/src/state.rs | 48 ++-- 6 files changed, 285 insertions(+), 123 deletions(-) create mode 100644 modules/block_kes_validator/src/ouroboros/kes_validation.rs create mode 100644 modules/block_kes_validator/src/ouroboros/tpraos.rs diff --git a/common/src/validation.rs b/common/src/validation.rs index 60bc15e0..d2e9be7e 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -235,6 +235,9 @@ pub enum KesValidationError { /// **Cause:** The operational certificate is invalid. #[error("Operational Certificate Error: {0}")] OperationalCertificateError(#[from] OperationalCertificateError), + /// **Cause:** No OCert counter found for this issuer (not a stake pool or genesis delegate) + #[error("No OCert Counter For Issuer: Pool ID={}", hex::encode(pool_id))] + NoOCertCounter { pool_id: PoolId }, /// **Cause:** Some data has incorrect bytes #[error("TryFromSlice: {0}")] TryFromSlice(String), @@ -242,6 +245,9 @@ pub enum KesValidationError { Other(String), } +/// Validation function for Kes +pub type KesValidation<'a> = Box Result<(), KesValidationError> + Send + Sync + 'a>; + #[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum KesSignatureError { /// **Cause:** Current KES period is before the operational certificate's diff --git a/modules/block_kes_validator/src/ouroboros/kes_validation.rs b/modules/block_kes_validator/src/ouroboros/kes_validation.rs new file mode 100644 index 00000000..0ff13629 --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/kes_validation.rs @@ -0,0 +1,231 @@ +use acropolis_common::{ + crypto::keyhash_224, + validation::{ + KesSignatureError, KesValidation, KesValidationError, OperationalCertificateError, + }, + GenesisDelegates, PoolId, +}; +use imbl::HashMap; +use pallas::{crypto::key::ed25519, ledger::traverse::MultiEraHeader}; + +use crate::ouroboros::{kes, praos, tpraos}; + +#[derive(Copy, Clone)] +pub struct OperationalCertificate<'a> { + pub operational_cert_hot_vkey: &'a [u8], + pub operational_cert_sequence_number: u64, + pub operational_cert_kes_period: u64, + pub operational_cert_sigma: &'a [u8], +} + +pub fn validate_kes_signature( + slot_kes_period: u64, + opcert_kes_period: u64, + header_body: &[u8], + public_key: &kes::PublicKey, + signature: &kes::Signature, + max_kes_evolutions: u64, +) -> Result<(), KesSignatureError> { + if opcert_kes_period > slot_kes_period { + return Err(KesSignatureError::KesBeforeStartOcert { + ocert_start_period: opcert_kes_period, + current_period: slot_kes_period, + }); + } + + if slot_kes_period >= opcert_kes_period + max_kes_evolutions { + return Err(KesSignatureError::KesAfterEndOcert { + current_period: slot_kes_period, + ocert_start_period: opcert_kes_period, + max_kes_evolutions, + }); + } + + let kes_period = (slot_kes_period - opcert_kes_period) as u32; + + signature.verify(kes_period, public_key, header_body).map_err(|error| { + KesSignatureError::InvalidKesSignatureOcert { + current_period: slot_kes_period, + ocert_start_period: opcert_kes_period, + reason: error.to_string(), + } + })?; + + Ok(()) +} + +pub fn validate_operational_certificate<'a>( + certificate: OperationalCertificate<'a>, + issuer: &ed25519::PublicKey, + latest_sequence_number: u64, + is_praos: bool, +) -> Result<(), OperationalCertificateError> { + // Verify the Operational Certificate signature + let signature = + ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| { + OperationalCertificateError::MalformedSignatureOcert { + reason: error.to_string(), + } + })?; + + let declared_sequence_number = certificate.operational_cert_sequence_number; + + // Check the sequence number of the operational certificate. It should either be the same + // as the latest known sequence number for the issuer or one greater. + if declared_sequence_number < latest_sequence_number { + return Err(OperationalCertificateError::CounterTooSmallOcert { + latest_counter: latest_sequence_number, + declared_counter: declared_sequence_number, + }); + } + + // this is only for praos protocol + if is_praos && (declared_sequence_number - latest_sequence_number) > 1 { + return Err(OperationalCertificateError::CounterOverIncrementedOcert { + latest_counter: latest_sequence_number, + declared_counter: declared_sequence_number, + }); + } + + // The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period + let mut message = Vec::new(); + message.extend_from_slice(certificate.operational_cert_hot_vkey); + message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes()); + message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes()); + if !issuer.verify(&message, &signature) { + return Err(OperationalCertificateError::InvalidSignatureOcert { + issuer: issuer.as_ref().to_vec(), + }); + } + + Ok(()) +} + +pub fn validate_block_kes<'a>( + header: &'a MultiEraHeader, + ocert_counters: &'a HashMap, + active_spos: &'a [PoolId], + genesis_delegs: &'a GenesisDelegates, + slots_per_kes_period: u64, + max_kes_evolutions: u64, +) -> Result>, Box> { + let is_praos = matches!(header, MultiEraHeader::BabbageCompatible(_)); + + let issuer_vkey = header.issuer_vkey().ok_or(Box::new(KesValidationError::Other( + "Issuer Key is not set".to_string(), + )))?; + let issuer = ed25519::PublicKey::from( + <[u8; ed25519::PublicKey::SIZE]>::try_from(issuer_vkey) + .map_err(|_| Box::new(KesValidationError::Other("Invalid issuer key".to_string())))?, + ); + let pool_id = PoolId::from(keyhash_224(issuer_vkey)); + + let slot_kes_period = header.slot() / slots_per_kes_period; + let cert = operational_cert(header).ok_or(Box::new(KesValidationError::Other( + "Operational certificate is not set".to_string(), + )))?; + let body_sig = body_signature(header).ok_or(Box::new(KesValidationError::Other( + "Body signature is not set".to_string(), + )))?; + let raw_header_body = header.header_body_cbor().ok_or(Box::new(KesValidationError::Other( + "Header body is not set".to_string(), + )))?; + + let latest_sequence_number = if is_praos { + praos::latest_issue_no_praos(ocert_counters, active_spos, &pool_id) + } else { + tpraos::latest_issue_no_tpraos(ocert_counters, active_spos, genesis_delegs, &pool_id) + } + .ok_or(Box::new(KesValidationError::NoOCertCounter { pool_id }))?; + + Ok(vec![ + Box::new(move || { + validate_kes_signature( + slot_kes_period, + cert.operational_cert_kes_period, + raw_header_body, + &kes::PublicKey::try_from(cert.operational_cert_hot_vkey).map_err(|_| { + KesValidationError::Other( + "Invalid operational certificate hot vkey".to_string(), + ) + })?, + &kes::Signature::try_from(body_sig) + .map_err(|_| KesValidationError::Other("Invalid body signature".to_string()))?, + max_kes_evolutions, + )?; + Ok(()) + }), + Box::new(move || { + validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?; + Ok(()) + }), + ]) +} + +fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option> { + match header { + MultiEraHeader::ShelleyCompatible(x) => { + let cert = OperationalCertificate { + operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey, + operational_cert_sequence_number: x.header_body.operational_cert_sequence_number, + operational_cert_kes_period: x.header_body.operational_cert_kes_period, + operational_cert_sigma: &x.header_body.operational_cert_sigma, + }; + Some(cert) + } + MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate { + operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey, + operational_cert_sequence_number: x + .header_body + .operational_cert + .operational_cert_sequence_number, + operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period, + operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma, + }), + _ => None, + } +} + +fn body_signature<'a>(header: &'a MultiEraHeader) -> Option<&'a [u8]> { + match header { + MultiEraHeader::ShelleyCompatible(x) => Some(&x.body_signature), + MultiEraHeader::BabbageCompatible(x) => Some(&x.body_signature), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use acropolis_common::{genesis_values::GenesisValues, Era}; + use pallas::ledger::traverse::MultiEraHeader; + + use super::*; + + #[test] + fn test_4490511_block_produced_by_genesis_key() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_4490511: Vec = + hex::decode(include_str!("./data/4490511.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4490511).unwrap(); + + let ocert_counters = HashMap::new(); + let active_spos = Vec::new(); + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } +} diff --git a/modules/block_kes_validator/src/ouroboros/mod.rs b/modules/block_kes_validator/src/ouroboros/mod.rs index a72745d0..26539d06 100644 --- a/modules/block_kes_validator/src/ouroboros/mod.rs +++ b/modules/block_kes_validator/src/ouroboros/mod.rs @@ -1,2 +1,4 @@ pub mod kes; +pub mod kes_validation; pub mod praos; +pub mod tpraos; \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/praos.rs b/modules/block_kes_validator/src/ouroboros/praos.rs index 72bef0ba..3fdea930 100644 --- a/modules/block_kes_validator/src/ouroboros/praos.rs +++ b/modules/block_kes_validator/src/ouroboros/praos.rs @@ -1,94 +1,14 @@ -use acropolis_common::validation::{KesSignatureError, OperationalCertificateError}; -use pallas::crypto::key::ed25519; - -use crate::ouroboros::kes; - -pub struct OperationalCertificate<'a> { - pub operational_cert_hot_vkey: &'a [u8], - pub operational_cert_sequence_number: u64, - pub operational_cert_kes_period: u64, - pub operational_cert_sigma: &'a [u8], -} - -pub fn validate_kes_signature<'a>( - slot_kes_period: u64, - opcert_kes_period: u64, - header_body: &[u8], - public_key: &kes::PublicKey, - signature: &kes::Signature, - max_kes_evolutions: u64, -) -> Result<(), KesSignatureError> { - if opcert_kes_period > slot_kes_period { - return Err(KesSignatureError::KesBeforeStartOcert { - ocert_start_period: opcert_kes_period, - current_period: slot_kes_period, - }); - } - - if slot_kes_period >= opcert_kes_period + max_kes_evolutions { - return Err(KesSignatureError::KesAfterEndOcert { - current_period: slot_kes_period, - ocert_start_period: opcert_kes_period, - max_kes_evolutions, - }); - } - - let kes_period = (slot_kes_period - opcert_kes_period) as u32; - - signature.verify(kes_period, public_key, header_body).map_err(|error| { - KesSignatureError::InvalidKesSignatureOcert { - current_period: slot_kes_period, - ocert_start_period: opcert_kes_period, - reason: error.to_string(), - } - })?; - - Ok(()) -} - -pub fn validate_operational_certificate<'a>( - certificate: OperationalCertificate<'a>, - issuer: &ed25519::PublicKey, - latest_sequence_number: u64, - is_praos: bool, -) -> Result<(), OperationalCertificateError> { - // Verify the Operational Certificate signature - let signature = - ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| { - OperationalCertificateError::MalformedSignatureOcert { - reason: error.to_string(), - } - })?; - - let declared_sequence_number = certificate.operational_cert_sequence_number; - - // Check the sequence number of the operational certificate. It should either be the same - // as the latest known sequence number for the issuer or one greater. - if declared_sequence_number < latest_sequence_number { - return Err(OperationalCertificateError::CounterTooSmallOcert { - latest_counter: latest_sequence_number, - declared_counter: declared_sequence_number, - }); - } - - // this is only for praos protocol - if is_praos && (declared_sequence_number - latest_sequence_number) > 1 { - return Err(OperationalCertificateError::CounterOverIncrementedOcert { - latest_counter: latest_sequence_number, - declared_counter: declared_sequence_number, - }); - } - - // The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period - let mut message = Vec::new(); - message.extend_from_slice(certificate.operational_cert_hot_vkey); - message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes()); - message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes()); - if !issuer.verify(&message, &signature) { - return Err(OperationalCertificateError::InvalidSignatureOcert { - issuer: issuer.as_ref().to_vec(), - }); - } - - Ok(()) +use acropolis_common::PoolId; +use imbl::HashMap; + +pub fn latest_issue_no_praos( + ocert_counter: &HashMap, + active_spos: &[PoolId], + pool_id: &PoolId, +) -> Option { + ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) { + Some(0) + } else { + None + }) } diff --git a/modules/block_kes_validator/src/ouroboros/tpraos.rs b/modules/block_kes_validator/src/ouroboros/tpraos.rs new file mode 100644 index 00000000..7426e77d --- /dev/null +++ b/modules/block_kes_validator/src/ouroboros/tpraos.rs @@ -0,0 +1,15 @@ +use acropolis_common::{GenesisDelegates, PoolId}; +use imbl::HashMap; + +pub fn latest_issue_no_tpraos( + ocert_counter: &HashMap, + active_spos: &[PoolId], + genesis_delegs: &GenesisDelegates, + pool_id: &PoolId, +) -> Option { + ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) { + Some(0) + } else { + genesis_delegs.as_ref().values().any(|v| v.delegate.eq(pool_id.as_ref())).then_some(0) + }) +} diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index 57f11fce..eb2a744a 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -1,12 +1,13 @@ -use crate::ouroboros::praos::OperationalCertificate; use acropolis_common::{ genesis_values::GenesisValues, messages::ProtocolParamsMessage, validation::KesValidationError, BlockInfo, PoolId, }; use imbl::HashMap; -use pallas::ledger::{primitives::babbage::OperationalCert, traverse::MultiEraHeader}; +use pallas::ledger::traverse::MultiEraHeader; use tracing::error; +use crate::ouroboros; + #[derive(Default, Debug, Clone)] pub struct State { pub ocert_counters: HashMap, @@ -14,6 +15,8 @@ pub struct State { pub slots_per_kes_period: Option, pub max_kes_evolutions: Option, + + pub active_spos: Vec, } impl State { @@ -22,6 +25,7 @@ impl State { ocert_counters: HashMap::new(), slots_per_kes_period: None, max_kes_evolutions: None, + active_spos: Vec::new(), } } @@ -65,34 +69,18 @@ impl State { ))); }; - let cert = operational_cert(&header).ok_or(Box::new(KesValidationError::Other( - "Can't get operational certificate".to_string(), - )))?; - - Ok(()) - } -} + let result = ouroboros::kes_validation::validate_block_kes( + &header, + &self.ocert_counters, + &self.active_spos, + &genesis.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); -fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option> { - match header { - MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate { - operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey, - operational_cert_sequence_number: x - .header_body - .operational_cert - .operational_cert_sequence_number, - operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period, - operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma, - }), - MultiEraHeader::ShelleyCompatible(x) => { - let cert = OperationalCertificate { - operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey, - operational_cert_sequence_number: x.header_body.operational_cert_sequence_number, - operational_cert_kes_period: x.header_body.operational_cert_kes_period, - operational_cert_sigma: &x.header_body.operational_cert_sigma, - }; - Some(cert) - } - _ => None, + result } } From 80e851cd845c4d4c634087c9520fcb96a8a294fe Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 14:12:43 +0100 Subject: [PATCH 06/15] fix: cargo shear --- Cargo.lock | 2 -- modules/block_kes_validator/Cargo.toml | 2 -- processes/omnibus/src/main.rs | 2 ++ 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ab72204..e5fdb7bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,8 +116,6 @@ dependencies = [ "imbl", "kes-summed-ed25519 0.2.1 (git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78)", "pallas 0.33.0", - "serde", - "serde_json", "thiserror 2.0.17", "tokio", "tracing", diff --git a/modules/block_kes_validator/Cargo.toml b/modules/block_kes_validator/Cargo.toml index db028af0..f307dd8c 100644 --- a/modules/block_kes_validator/Cargo.toml +++ b/modules/block_kes_validator/Cargo.toml @@ -19,8 +19,6 @@ hex = { workspace = true } imbl = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } thiserror = "2.0.17" pallas = { workspace = true } diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index a978dbc3..7498db7d 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -11,6 +11,7 @@ use tracing::info; use acropolis_module_accounts_state::AccountsState; use acropolis_module_address_state::AddressState; use acropolis_module_assets_state::AssetsState; +use acropolis_module_block_kes_validator::BlockKesValidator; use acropolis_module_block_unpacker::BlockUnpacker; use acropolis_module_block_vrf_validator::BlockVrfValidator; use acropolis_module_chain_store::ChainStore; @@ -121,6 +122,7 @@ pub async fn main() -> Result<()> { Consensus::register(&mut process); ChainStore::register(&mut process); BlockVrfValidator::register(&mut process); + BlockKesValidator::register(&mut process); Clock::::register(&mut process); RESTServer::::register(&mut process); From bb8642f517f85e5ecfae77df4ed0b329b4fccabb Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 14:26:56 +0100 Subject: [PATCH 07/15] refactor: update active spos on kes validator --- .../src/block_kes_validator.rs | 39 +++++++++++++++---- modules/block_kes_validator/src/state.rs | 8 +++- .../src/block_vrf_validator.rs | 4 +- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs index 9094de0e..74b2b517 100644 --- a/modules/block_kes_validator/src/block_kes_validator.rs +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -26,12 +26,14 @@ const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( "bootstrapped-subscribe-topic", "cardano.sequence.bootstrapped", ); +const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = + ("blocks-subscribe-topic", "cardano.block.proposed"); const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( "protocol-parameters-subscribe-topic", "cardano.protocol.parameters", ); -const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = - ("blocks-subscribe-topic", "cardano.block.proposed"); +const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = + ("spo-state-subscribe-topic", "cardano.spo.state"); /// Block KES Validator module #[module( @@ -50,6 +52,7 @@ impl BlockKesValidator { mut bootstrapped_subscription: Box>, mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, + mut spo_state_subscription: Box>, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -80,6 +83,7 @@ impl BlockKesValidator { if is_new_epoch { // read epoch boundary messages let protocol_parameters_message_f = protocol_parameters_subscription.read(); + let spo_state_message_f = spo_state_subscription.read(); let (_, protocol_parameters_msg) = protocol_parameters_message_f.await?; let span = info_span!( @@ -93,6 +97,19 @@ impl BlockKesValidator { } _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), }); + + let (_, spo_state_msg) = spo_state_message_f.await?; + let span = info_span!( + "block_kes_validator.handle_spo_state", + epoch = block_info.epoch + ); + span.in_scope(|| match spo_state_msg.as_ref() { + Message::Cardano((block_info, CardanoMessage::SPOState(msg))) => { + Self::check_sync(¤t_block, block_info); + state.handle_spo_state(msg); + } + _ => error!("Unexpected message type: {spo_state_msg:?}"), + }); } let span = @@ -133,25 +150,32 @@ impl BlockKesValidator { .get_string(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.0) .unwrap_or(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating subscriber for bootstrapped on '{bootstrapped_subscribe_topic}'"); - let protocol_parameters_subscribe_topic = config - .get_string(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.0) - .unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.1.to_string()); - info!("Creating subscriber for protocol parameters on '{protocol_parameters_subscribe_topic}'"); let blocks_subscribe_topic = config .get_string(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.0) .unwrap_or(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating blocks subscription on '{blocks_subscribe_topic}'"); + let protocol_parameters_subscribe_topic = config + .get_string(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating subscriber for protocol parameters on '{protocol_parameters_subscribe_topic}'"); + + let spo_state_subscribe_topic = config + .get_string(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating spo state subscription on '{spo_state_subscribe_topic}'"); + // publishers let kes_validation_publisher = KesValidationPublisher::new(context.clone(), validation_kes_publisher_topic); // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; + let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; let protocol_parameters_subscription = context.subscribe(&protocol_parameters_subscribe_topic).await?; - let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; + let spo_state_subscription = context.subscribe(&spo_state_subscribe_topic).await?; // state history let history = Arc::new(Mutex::new(StateHistory::::new( @@ -167,6 +191,7 @@ impl BlockKesValidator { bootstrapped_subscription, blocks_subscription, protocol_parameters_subscription, + spo_state_subscription, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index eb2a744a..120ff3ae 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -1,5 +1,7 @@ use acropolis_common::{ - genesis_values::GenesisValues, messages::ProtocolParamsMessage, validation::KesValidationError, + genesis_values::GenesisValues, + messages::{ProtocolParamsMessage, SPOStateMessage}, + validation::KesValidationError, BlockInfo, PoolId, }; use imbl::HashMap; @@ -36,6 +38,10 @@ impl State { } } + pub fn handle_spo_state(&mut self, msg: &SPOStateMessage) { + self.active_spos = msg.spos.iter().map(|spo| spo.operator).collect(); + } + pub fn validate_block_kes( &self, block_info: &BlockInfo, diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 69ecd04f..2f202293 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -27,12 +27,12 @@ const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( "bootstrapped-subscribe-topic", "cardano.sequence.bootstrapped", ); +const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = + ("blocks-subscribe-topic", "cardano.block.proposed"); const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( "protocol-parameters-subscribe-topic", "cardano.protocol.parameters", ); -const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = - ("blocks-subscribe-topic", "cardano.block.proposed"); const DEFAULT_EPOCH_ACTIVITY_SUBSCRIBE_TOPIC: (&str, &str) = ("epoch-activity-subscribe-topic", "cardano.epoch.activity"); const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = From 890445c718f296713e3dd48ceda2af7126db4ff8 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 14:29:49 +0100 Subject: [PATCH 08/15] fix: cargo fmt --- modules/block_kes_validator/src/ouroboros/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/block_kes_validator/src/ouroboros/mod.rs b/modules/block_kes_validator/src/ouroboros/mod.rs index 26539d06..1c6076b1 100644 --- a/modules/block_kes_validator/src/ouroboros/mod.rs +++ b/modules/block_kes_validator/src/ouroboros/mod.rs @@ -1,4 +1,4 @@ pub mod kes; pub mod kes_validation; pub mod praos; -pub mod tpraos; \ No newline at end of file +pub mod tpraos; From 007a358e4efd768a2862898f088a358faec3e8db Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 14:47:08 +0100 Subject: [PATCH 09/15] refactor: add more test cases - both tpraos and praos blocks --- .../src/ouroboros/data/4576496.cbor | 1 - .../src/ouroboros/kes_validation.rs | 206 +++++++++++++++++- 2 files changed, 204 insertions(+), 3 deletions(-) delete mode 100644 modules/block_kes_validator/src/ouroboros/data/4576496.cbor diff --git a/modules/block_kes_validator/src/ouroboros/data/4576496.cbor b/modules/block_kes_validator/src/ouroboros/data/4576496.cbor deleted file mode 100644 index c58d4f5f..00000000 --- a/modules/block_kes_validator/src/ouroboros/data/4576496.cbor +++ /dev/null @@ -1 +0,0 @@ -828f1a0045d4f01a005eebcd5820d0547673599cc31048011657b1ac2ae14ff15b2103702b4071fd0c0e1caaeecf5820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f610825840ce902041f973eb263968e1de7f6255de7a9ddcafa343c2d2ed89c4daaca121d87621f683634ef07ff8234f69e381bd593e6229e92b2416fb463e5b50f1df675a5850b6568f768f3e4ca83dee729f934b83438fd693945f6c08110c1d9963024c904c1f836b51792a839aaedeb9381f65273abee234ea9e3205810e08ec03413244e2345aa58992d60e30aa38c3191f11db03825840000f783edf754904b325197136c4cdf5badf6a32ee7b86758d005aed320eb298efcd137da7507a51a7c580211017b14b08267c947b179358ebd5ee77d6b473b4585051632cef6eb7a950d94086d8f8770028d32ff55dc39741f7d45e061fd558383d5e18eb15aa6192a9c0bd16a5dbfcbee0dc041d158a2c50e67e80d3fc4a78dab889a5b703ab815afece18d00ed0431f0019018c582040fd0376795eec5777148337d4a5d37b72c358c30da034de8bac5e23c4525407582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0c28b49147a742ee1a357f459315234446df337e2933df741307268fc064c32c0d465e2785f29ac9a79bfe55db85be4f3e36bfc2b7e15f76db91c46cbc2d27601b08c6020c1aee33d51ab7a66f9432d61d1dbb5bc3a7f6b183d414b94ffacbb459600a4ef2b27ece3c5682b50d1316dff928d7e627cb4f6754321e6465acb16cd360c41571150cd17dafdb642a27bbd5e80ce24a30ac2126a5c27fd866f7e43ab6c446de810e15924cf0b05873c6f4e49e518c139333d6d47c1ae709771ac5abf95270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/modules/block_kes_validator/src/ouroboros/kes_validation.rs b/modules/block_kes_validator/src/ouroboros/kes_validation.rs index 0ff13629..b842439d 100644 --- a/modules/block_kes_validator/src/ouroboros/kes_validation.rs +++ b/modules/block_kes_validator/src/ouroboros/kes_validation.rs @@ -196,7 +196,7 @@ fn body_signature<'a>(header: &'a MultiEraHeader) -> Option<&'a [u8]> { #[cfg(test)] mod tests { - use acropolis_common::{genesis_values::GenesisValues, Era}; + use acropolis_common::{genesis_values::GenesisValues, serialization::Bech32Conversion, Era}; use pallas::ledger::traverse::MultiEraHeader; use super::*; @@ -213,7 +213,7 @@ mod tests { MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4490511).unwrap(); let ocert_counters = HashMap::new(); - let active_spos = Vec::new(); + let active_spos = vec![]; let result = validate_block_kes( &block_header, @@ -228,4 +228,206 @@ mod tests { }); assert!(result.is_ok()); } + + #[test] + fn test_4556956_block() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_4556956: Vec = + hex::decode(include_str!("./data/4556956.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4556956).unwrap(); + + let ocert_counters = HashMap::from_iter([( + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(), + 1, + )]); + let active_spos = + vec![ + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(), + ]; + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } + + #[test] + fn test_4556956_block_with_wrong_ocert_counter() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_4556956: Vec = + hex::decode(include_str!("./data/4556956.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4556956).unwrap(); + + let ocert_counters = HashMap::from_iter([( + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(), + 2, + )]); + let active_spos = + vec![ + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(), + ]; + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + Box::new(KesValidationError::OperationalCertificateError( + OperationalCertificateError::CounterTooSmallOcert { + latest_counter: 2, + declared_counter: 1 + } + )) + ); + } + + #[test] + fn test_4556956_block_with_missing_ocert_counter_and_active_spos() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_4556956: Vec = + hex::decode(include_str!("./data/4556956.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4556956).unwrap(); + + let ocert_counters = HashMap::new(); + let active_spos = vec![]; + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + Box::new(KesValidationError::NoOCertCounter { + pool_id: PoolId::from_bech32( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy" + ) + .unwrap(), + }) + ); + } + + #[test] + fn test_7854823_praos_block() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_7854823: Vec = + hex::decode(include_str!("./data/7854823.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Babbage as u8, None, &block_header_7854823).unwrap(); + + let ocert_counters = HashMap::from_iter([( + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(), + 11, + )]); + let active_spos = + vec![ + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(), + ]; + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } + + #[test] + fn test_7854823_praos_block_with_overincremented_ocert_counter() { + let slots_per_kes_period = 129600; + let max_kes_evolutions = 62; + let genesis_values = GenesisValues::mainnet(); + + let block_header_7854823: Vec = + hex::decode(include_str!("./data/7854823.cbor")).unwrap(); + let block_header = + MultiEraHeader::decode(Era::Babbage as u8, None, &block_header_7854823).unwrap(); + + let ocert_counters = HashMap::from_iter([( + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(), + // This is just for test case + // actual on-chain value is 11 + // now ocert counter is incremented by 2 + 9, + )]); + let active_spos = + vec![ + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(), + ]; + + let result = validate_block_kes( + &block_header, + &ocert_counters, + &active_spos, + &genesis_values.genesis_delegs, + slots_per_kes_period, + max_kes_evolutions, + ) + .and_then(|kes_validations| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + Box::new(KesValidationError::OperationalCertificateError( + OperationalCertificateError::CounterOverIncrementedOcert { + latest_counter: 9, + declared_counter: 11, + } + )) + ); + } } From 7ddd2f094e9383e0ca488859b4a4949a9cf3ed14 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 15:59:34 +0100 Subject: [PATCH 10/15] feat: update ocert counters when block is validated successfully --- .../src/block_kes_validator.rs | 9 + .../src/kes_validation_publisher.rs | 4 +- .../src/ouroboros/kes_validation.rs | 155 ++++++++++-------- .../src/ouroboros/praos.rs | 8 +- .../src/ouroboros/tpraos.rs | 14 +- modules/block_kes_validator/src/state.rs | 20 ++- 6 files changed, 132 insertions(+), 78 deletions(-) diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs index 74b2b517..caafdc7e 100644 --- a/modules/block_kes_validator/src/block_kes_validator.rs +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -118,6 +118,15 @@ impl BlockKesValidator { let result = state .validate_block_kes(block_info, &block_msg.header, &genesis) .map_err(|e| *e); + + // Update the operational certificate counter + // When block is validated successfully + // Reference + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L508 + if let Ok(Some((pool_id, updated_sequence_number))) = result.as_ref() { + state.update_ocert_counter(*pool_id, *updated_sequence_number); + } + if let Err(e) = kes_validation_publisher .publish_kes_validation(block_info, result) .await diff --git a/modules/block_kes_validator/src/kes_validation_publisher.rs b/modules/block_kes_validator/src/kes_validation_publisher.rs index 2b92c53e..b745e5de 100644 --- a/modules/block_kes_validator/src/kes_validation_publisher.rs +++ b/modules/block_kes_validator/src/kes_validation_publisher.rs @@ -1,7 +1,7 @@ use acropolis_common::{ messages::{CardanoMessage, Message}, validation::{KesValidationError, ValidationError, ValidationStatus}, - BlockInfo, + BlockInfo, PoolId, }; use caryatid_sdk::Context; use std::sync::Arc; @@ -25,7 +25,7 @@ impl KesValidationPublisher { pub async fn publish_kes_validation( &mut self, block: &BlockInfo, - validation_result: Result<(), KesValidationError>, + validation_result: Result, KesValidationError>, ) -> anyhow::Result<()> { let validation_status = match validation_result { Ok(_) => ValidationStatus::Go, diff --git a/modules/block_kes_validator/src/ouroboros/kes_validation.rs b/modules/block_kes_validator/src/ouroboros/kes_validation.rs index b842439d..1c2633a6 100644 --- a/modules/block_kes_validator/src/ouroboros/kes_validation.rs +++ b/modules/block_kes_validator/src/ouroboros/kes_validation.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use acropolis_common::{ crypto::keyhash_224, validation::{ @@ -12,9 +14,13 @@ use crate::ouroboros::{kes, praos, tpraos}; #[derive(Copy, Clone)] pub struct OperationalCertificate<'a> { + /// The operational hot key pub operational_cert_hot_vkey: &'a [u8], + /// The sequence number of the operational certificate pub operational_cert_sequence_number: u64, + /// The KES period of the operational certificate pub operational_cert_kes_period: u64, + /// The signature of the operational certificate pub operational_cert_sigma: &'a [u8], } @@ -88,6 +94,8 @@ pub fn validate_operational_certificate<'a>( } // The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period + // Reference + // https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/OCert.hs#L144 let mut message = Vec::new(); message.extend_from_slice(certificate.operational_cert_hot_vkey); message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes()); @@ -101,36 +109,45 @@ pub fn validate_operational_certificate<'a>( Ok(()) } +/// This function check block header's KES signature and operational certificate +/// return validation functions for KES signature and operational certificate +/// and the pool id and declared sequence number (which will be used to update the operational certificate counter when validation is successful) +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L612 pub fn validate_block_kes<'a>( header: &'a MultiEraHeader, ocert_counters: &'a HashMap, - active_spos: &'a [PoolId], + active_spos: &'a HashSet, genesis_delegs: &'a GenesisDelegates, slots_per_kes_period: u64, max_kes_evolutions: u64, -) -> Result>, Box> { +) -> Result<(Vec>, PoolId, u64), Box> { let is_praos = matches!(header, MultiEraHeader::BabbageCompatible(_)); let issuer_vkey = header.issuer_vkey().ok_or(Box::new(KesValidationError::Other( - "Issuer Key is not set".to_string(), + "Block header missing issuer verification key".to_string(), )))?; let issuer = ed25519::PublicKey::from( - <[u8; ed25519::PublicKey::SIZE]>::try_from(issuer_vkey) - .map_err(|_| Box::new(KesValidationError::Other("Invalid issuer key".to_string())))?, + <[u8; ed25519::PublicKey::SIZE]>::try_from(issuer_vkey).map_err(|_| { + Box::new(KesValidationError::Other( + "Issuer verification key has invalid length (expected 32 bytes)".to_string(), + )) + })?, ); let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let slot_kes_period = header.slot() / slots_per_kes_period; let cert = operational_cert(header).ok_or(Box::new(KesValidationError::Other( - "Operational certificate is not set".to_string(), + "Block header missing operational certificate".to_string(), )))?; let body_sig = body_signature(header).ok_or(Box::new(KesValidationError::Other( - "Body signature is not set".to_string(), + "Block header missing KES body signature".to_string(), )))?; let raw_header_body = header.header_body_cbor().ok_or(Box::new(KesValidationError::Other( - "Header body is not set".to_string(), + "Block header body CBOR not available".to_string(), )))?; + let declared_sequence_number = cert.operational_cert_sequence_number; let latest_sequence_number = if is_praos { praos::latest_issue_no_praos(ocert_counters, active_spos, &pool_id) } else { @@ -138,28 +155,33 @@ pub fn validate_block_kes<'a>( } .ok_or(Box::new(KesValidationError::NoOCertCounter { pool_id }))?; - Ok(vec![ - Box::new(move || { - validate_kes_signature( - slot_kes_period, - cert.operational_cert_kes_period, - raw_header_body, - &kes::PublicKey::try_from(cert.operational_cert_hot_vkey).map_err(|_| { - KesValidationError::Other( - "Invalid operational certificate hot vkey".to_string(), - ) - })?, - &kes::Signature::try_from(body_sig) - .map_err(|_| KesValidationError::Other("Invalid body signature".to_string()))?, - max_kes_evolutions, - )?; - Ok(()) - }), - Box::new(move || { - validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?; - Ok(()) - }), - ]) + Ok(( + vec![ + Box::new(move || { + validate_kes_signature( + slot_kes_period, + cert.operational_cert_kes_period, + raw_header_body, + &kes::PublicKey::try_from(cert.operational_cert_hot_vkey).map_err(|_| { + KesValidationError::Other( + "Invalid operational certificate hot vkey".to_string(), + ) + })?, + &kes::Signature::try_from(body_sig).map_err(|_| { + KesValidationError::Other("Invalid body signature".to_string()) + })?, + max_kes_evolutions, + )?; + Ok(()) + }), + Box::new(move || { + validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?; + Ok(()) + }), + ], + pool_id, + declared_sequence_number, + )) } fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option> { @@ -213,7 +235,7 @@ mod tests { MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4490511).unwrap(); let ocert_counters = HashMap::new(); - let active_spos = vec![]; + let active_spos = HashSet::new(); let result = validate_block_kes( &block_header, @@ -223,10 +245,12 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); assert!(result.is_ok()); + assert_eq!(result.unwrap().1, 0); } #[test] @@ -245,11 +269,10 @@ mod tests { .unwrap(), 1, )]); - let active_spos = - vec![ - PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") - .unwrap(), - ]; + let active_spos = HashSet::from_iter([PoolId::from_bech32( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + ) + .unwrap()]); let result = validate_block_kes( &block_header, @@ -259,10 +282,12 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); assert!(result.is_ok()); + assert_eq!(result.unwrap().1, 1); } #[test] @@ -281,11 +306,10 @@ mod tests { .unwrap(), 2, )]); - let active_spos = - vec![ - PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") - .unwrap(), - ]; + let active_spos = HashSet::from_iter([PoolId::from_bech32( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + ) + .unwrap()]); let result = validate_block_kes( &block_header, @@ -295,8 +319,9 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); assert!(result.is_err()); assert_eq!( @@ -322,7 +347,7 @@ mod tests { MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4556956).unwrap(); let ocert_counters = HashMap::new(); - let active_spos = vec![]; + let active_spos = HashSet::new(); let result = validate_block_kes( &block_header, @@ -332,9 +357,11 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); + assert!(result.is_err()); assert_eq!( result.unwrap_err(), @@ -363,11 +390,10 @@ mod tests { .unwrap(), 11, )]); - let active_spos = - vec![ - PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") - .unwrap(), - ]; + let active_spos = HashSet::from_iter([PoolId::from_bech32( + "pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t", + ) + .unwrap()]); let result = validate_block_kes( &block_header, @@ -377,8 +403,9 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); assert!(result.is_ok()); } @@ -402,11 +429,10 @@ mod tests { // now ocert counter is incremented by 2 9, )]); - let active_spos = - vec![ - PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") - .unwrap(), - ]; + let active_spos = HashSet::from_iter([PoolId::from_bech32( + "pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t", + ) + .unwrap()]); let result = validate_block_kes( &block_header, @@ -416,8 +442,9 @@ mod tests { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok((pool_id, declared_sequence_number)) }); assert!(result.is_err()); assert_eq!( diff --git a/modules/block_kes_validator/src/ouroboros/praos.rs b/modules/block_kes_validator/src/ouroboros/praos.rs index 3fdea930..5abce948 100644 --- a/modules/block_kes_validator/src/ouroboros/praos.rs +++ b/modules/block_kes_validator/src/ouroboros/praos.rs @@ -1,12 +1,14 @@ +use std::collections::HashSet; + use acropolis_common::PoolId; use imbl::HashMap; pub fn latest_issue_no_praos( - ocert_counter: &HashMap, - active_spos: &[PoolId], + ocert_counters: &HashMap, + active_spos: &HashSet, pool_id: &PoolId, ) -> Option { - ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) { + ocert_counters.get(pool_id).copied().or(if active_spos.contains(pool_id) { Some(0) } else { None diff --git a/modules/block_kes_validator/src/ouroboros/tpraos.rs b/modules/block_kes_validator/src/ouroboros/tpraos.rs index 7426e77d..b7366a50 100644 --- a/modules/block_kes_validator/src/ouroboros/tpraos.rs +++ b/modules/block_kes_validator/src/ouroboros/tpraos.rs @@ -1,13 +1,21 @@ +use std::collections::HashSet; + use acropolis_common::{GenesisDelegates, PoolId}; use imbl::HashMap; +/// This function is used to get the latest issue number for a given pool id. +/// First check ocert_counters +/// Check if the pool is in active_spos (registered or not) +/// And if the pool is a genesis delegate +/// Reference +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/OCert.hs#L66 pub fn latest_issue_no_tpraos( - ocert_counter: &HashMap, - active_spos: &[PoolId], + ocert_counters: &HashMap, + active_spos: &HashSet, genesis_delegs: &GenesisDelegates, pool_id: &PoolId, ) -> Option { - ocert_counter.get(pool_id).copied().or(if active_spos.contains(pool_id) { + ocert_counters.get(pool_id).copied().or(if active_spos.contains(pool_id) { Some(0) } else { genesis_delegs.as_ref().values().any(|v| v.delegate.eq(pool_id.as_ref())).then_some(0) diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index 120ff3ae..6eb8fc6a 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use acropolis_common::{ genesis_values::GenesisValues, messages::{ProtocolParamsMessage, SPOStateMessage}, @@ -12,13 +14,14 @@ use crate::ouroboros; #[derive(Default, Debug, Clone)] pub struct State { + /// Tracks the latest operational certificate counter for each pool pub ocert_counters: HashMap, pub slots_per_kes_period: Option, pub max_kes_evolutions: Option, - pub active_spos: Vec, + pub active_spos: HashSet, } impl State { @@ -27,7 +30,7 @@ impl State { ocert_counters: HashMap::new(), slots_per_kes_period: None, max_kes_evolutions: None, - active_spos: Vec::new(), + active_spos: HashSet::new(), } } @@ -42,15 +45,19 @@ impl State { self.active_spos = msg.spos.iter().map(|spo| spo.operator).collect(); } + pub fn update_ocert_counter(&mut self, pool_id: PoolId, declared_sequence_number: u64) { + self.ocert_counters.insert(pool_id, declared_sequence_number); + } + pub fn validate_block_kes( &self, block_info: &BlockInfo, raw_header: &[u8], genesis: &GenesisValues, - ) -> Result<(), Box> { + ) -> Result, Box> { // Validation starts after Shelley Era if block_info.epoch < genesis.shelley_epoch { - return Ok(()); + return Ok(None); } let header = match MultiEraHeader::decode(block_info.era as u8, None, raw_header) { @@ -83,8 +90,9 @@ impl State { slots_per_kes_period, max_kes_evolutions, ) - .and_then(|kes_validations| { - kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + .and_then(|(kes_validations, pool_id, declared_sequence_number)| { + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new))?; + Ok(Some((pool_id, declared_sequence_number))) }); result From 4d1e300b1314d186c0ed0b8f5ae2ea67b02f639c Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 12 Nov 2025 16:39:49 +0100 Subject: [PATCH 11/15] fix: log kes validation with details --- common/src/validation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/validation.rs b/common/src/validation.rs index d2e9be7e..5583ec7d 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -15,7 +15,7 @@ pub enum ValidationError { #[error("VRF failure: {0}")] BadVRF(#[from] VrfValidationError), - #[error("KES failure")] + #[error("KES failure: {0}")] BadKES(#[from] KesValidationError), #[error("Doubly spent UTXO: {0}")] From 3c6ee83c51a5bd59a887b2a55310528efb94f6a4 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Sun, 16 Nov 2025 21:09:17 +0100 Subject: [PATCH 12/15] fix: add pool id to InvalidSignatureOcert error --- common/src/validation.rs | 4 ++-- .../src/ouroboros/kes_validation.rs | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/src/validation.rs b/common/src/validation.rs index 5583ec7d..a33ebae9 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -295,8 +295,8 @@ pub enum OperationalCertificateError { MalformedSignatureOcert { reason: String }, /// **Cause:** The cold key signature on the operational certificate is invalid. /// The OCert was not properly signed by the pool's cold key. - #[error("Invalid Signature OCert: Issuer={}", hex::encode(issuer))] - InvalidSignatureOcert { issuer: Vec }, + #[error("Invalid Signature OCert: Issuer={}, Pool ID={}", hex::encode(issuer), hex::encode(pool_id))] + InvalidSignatureOcert { issuer: Vec, pool_id: PoolId, }, /// **Cause:** The operational certificate counter in the header is not greater /// than the last counter used by this pool. #[error( diff --git a/modules/block_kes_validator/src/ouroboros/kes_validation.rs b/modules/block_kes_validator/src/ouroboros/kes_validation.rs index 1c2633a6..34ef56ac 100644 --- a/modules/block_kes_validator/src/ouroboros/kes_validation.rs +++ b/modules/block_kes_validator/src/ouroboros/kes_validation.rs @@ -62,6 +62,7 @@ pub fn validate_kes_signature( pub fn validate_operational_certificate<'a>( certificate: OperationalCertificate<'a>, + pool_id: &PoolId, issuer: &ed25519::PublicKey, latest_sequence_number: u64, is_praos: bool, @@ -103,6 +104,7 @@ pub fn validate_operational_certificate<'a>( if !issuer.verify(&message, &signature) { return Err(OperationalCertificateError::InvalidSignatureOcert { issuer: issuer.as_ref().to_vec(), + pool_id: *pool_id, }); } @@ -175,7 +177,13 @@ pub fn validate_block_kes<'a>( Ok(()) }), Box::new(move || { - validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?; + validate_operational_certificate( + cert, + &pool_id, + &issuer, + latest_sequence_number, + is_praos, + )?; Ok(()) }), ], From 46a107c606930980bf63bc2498fc9d60784066e7 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Sun, 16 Nov 2025 21:12:42 +0100 Subject: [PATCH 13/15] fix: move unnecessary functions to test section --- .../block_kes_validator/src/ouroboros/kes.rs | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/modules/block_kes_validator/src/ouroboros/kes.rs b/modules/block_kes_validator/src/ouroboros/kes.rs index dfe76ded..0ecbfb2d 100644 --- a/modules/block_kes_validator/src/ouroboros/kes.rs +++ b/modules/block_kes_validator/src/ouroboros/kes.rs @@ -1,37 +1,7 @@ -use kes_summed_ed25519::{ - self as kes, - kes::{Sum6Kes, Sum6KesSig}, - traits::{KesSig, KesSk}, -}; +use kes_summed_ed25519::{self as kes, kes::Sum6KesSig, traits::KesSig}; use std::{array::TryFromSliceError, ops::Deref}; use thiserror::Error; -// ------------------------------------------------------------------- SecretKey - -/// KES secret key -pub struct SecretKey<'a>(Sum6Kes<'a>); - -impl SecretKey<'_> { - /// Create a new KES secret key - pub fn from_bytes(sk_bytes: &mut Vec) -> Result, Error> { - // TODO: extend() could potentially re-allocate memory to a new location and copy the sk_bytes. - // This would leave the original memory containing the secret key without being wiped. - sk_bytes.extend([0u8; 4]); // default to period = 0 - let sum_6_kes = Sum6Kes::from_bytes(sk_bytes.as_mut_slice())?; - Ok(SecretKey(sum_6_kes)) - } - - /// Get the current period of the KES secret key - pub fn get_period(&self) -> u32 { - self.0.get_period() - } - - /// Update the KES secret key to the next period - pub fn update(&mut self) -> Result<(), Error> { - Ok(self.0.update()?) - } -} - // ------------------------------------------------------------------- PublicKey /// KES public key @@ -61,12 +31,6 @@ impl Deref for PublicKey { } } -impl From<&SecretKey<'_>> for PublicKey { - fn from(sk: &SecretKey<'_>) -> Self { - PublicKey(sk.0.to_pk()) - } -} - impl From<&[u8; PublicKey::SIZE]> for PublicKey { fn from(bytes: &[u8; PublicKey::SIZE]) -> Self { PublicKey(kes::PublicKey::from_bytes(bytes).unwrap_or_else(|e| { @@ -135,6 +99,39 @@ pub enum Error { #[cfg(test)] mod tests { use super::*; + use kes_summed_ed25519::{kes::Sum6Kes, traits::KesSk}; + + // ------------------------------------------------------------------- SecretKey + + /// KES secret key + pub struct SecretKey<'a>(Sum6Kes<'a>); + + impl SecretKey<'_> { + /// Create a new KES secret key + pub fn from_bytes(sk_bytes: &mut Vec) -> Result, Error> { + // TODO: extend() could potentially re-allocate memory to a new location and copy the sk_bytes. + // This would leave the original memory containing the secret key without being wiped. + sk_bytes.extend([0u8; 4]); // default to period = 0 + let sum_6_kes = Sum6Kes::from_bytes(sk_bytes.as_mut_slice())?; + Ok(SecretKey(sum_6_kes)) + } + + /// Get the current period of the KES secret key + pub fn get_period(&self) -> u32 { + self.0.get_period() + } + + /// Update the KES secret key to the next period + pub fn update(&mut self) -> Result<(), Error> { + Ok(self.0.update()?) + } + } + + impl From<&SecretKey<'_>> for PublicKey { + fn from(sk: &SecretKey<'_>) -> Self { + PublicKey(sk.0.to_pk()) + } + } #[test] fn kes_key_evolution() { From 17088d782798d2ef50fb1e6db0683250bc1c444f Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Sun, 16 Nov 2025 21:13:59 +0100 Subject: [PATCH 14/15] fix: remove mut for publisher --- modules/block_kes_validator/src/block_kes_validator.rs | 2 +- modules/block_kes_validator/src/kes_validation_publisher.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs index caafdc7e..25eafcb1 100644 --- a/modules/block_kes_validator/src/block_kes_validator.rs +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -48,7 +48,7 @@ impl BlockKesValidator { #[allow(clippy::too_many_arguments)] async fn run( history: Arc>>, - mut kes_validation_publisher: KesValidationPublisher, + kes_validation_publisher: KesValidationPublisher, mut bootstrapped_subscription: Box>, mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, diff --git a/modules/block_kes_validator/src/kes_validation_publisher.rs b/modules/block_kes_validator/src/kes_validation_publisher.rs index b745e5de..fe9254ea 100644 --- a/modules/block_kes_validator/src/kes_validation_publisher.rs +++ b/modules/block_kes_validator/src/kes_validation_publisher.rs @@ -23,7 +23,7 @@ impl KesValidationPublisher { } pub async fn publish_kes_validation( - &mut self, + &self, block: &BlockInfo, validation_result: Result, KesValidationError>, ) -> anyhow::Result<()> { From 6a4c2a3f429e324d148bbe58307620be206fd5a5 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Sun, 16 Nov 2025 21:14:57 +0100 Subject: [PATCH 15/15] fix: cargo fmt --- common/src/validation.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/common/src/validation.rs b/common/src/validation.rs index a33ebae9..d92c06bd 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -295,8 +295,12 @@ pub enum OperationalCertificateError { MalformedSignatureOcert { reason: String }, /// **Cause:** The cold key signature on the operational certificate is invalid. /// The OCert was not properly signed by the pool's cold key. - #[error("Invalid Signature OCert: Issuer={}, Pool ID={}", hex::encode(issuer), hex::encode(pool_id))] - InvalidSignatureOcert { issuer: Vec, pool_id: PoolId, }, + #[error( + "Invalid Signature OCert: Issuer={}, Pool ID={}", + hex::encode(issuer), + hex::encode(pool_id) + )] + InvalidSignatureOcert { issuer: Vec, pool_id: PoolId }, /// **Cause:** The operational certificate counter in the header is not greater /// than the last counter used by this pool. #[error(