From 174c9b1d39e3df5c094e2468191a804dc604b60c Mon Sep 17 00:00:00 2001 From: Andrew Westberg Date: Fri, 11 Oct 2024 16:34:12 +0000 Subject: [PATCH] feat: Validate conway block KES signatures --- ouroboros-praos/Cargo.toml | 8 +- ouroboros-praos/src/consensus/mod.rs | 226 ++++++++++++++++++++++----- ouroboros/Cargo.toml | 4 +- ouroboros/src/ledger/mod.rs | 12 +- 4 files changed, 202 insertions(+), 48 deletions(-) diff --git a/ouroboros-praos/Cargo.toml b/ouroboros-praos/Cargo.toml index 70fe068..f1e41a9 100644 --- a/ouroboros-praos/Cargo.toml +++ b/ouroboros-praos/Cargo.toml @@ -6,14 +6,14 @@ edition = "2021" [dependencies] hex = "0.4" ouroboros = { path = "../ouroboros" } -pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } -pallas-math = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } -pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } +pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } +pallas-math = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } +pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } tracing = "0.1" [dev-dependencies] ctor = "0.2" insta = { version = "1.40.0", features = ["yaml"] } mockall = "0.13" -pallas-traverse = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } +pallas-traverse = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } tracing-subscriber = "0.3" \ No newline at end of file diff --git a/ouroboros-praos/src/consensus/mod.rs b/ouroboros-praos/src/consensus/mod.rs index 4f064b8..8e76b71 100644 --- a/ouroboros-praos/src/consensus/mod.rs +++ b/ouroboros-praos/src/consensus/mod.rs @@ -1,6 +1,8 @@ -use ouroboros::ledger::{issuer_vkey_to_pool_id, PoolId, PoolInfo}; +use ouroboros::ledger::{issuer_vkey_to_pool_id, LedgerState, PoolId}; use ouroboros::validator::Validator; use pallas_crypto::hash::{Hash, Hasher}; +use pallas_crypto::kes::{KesPublicKey, KesSignature}; +use pallas_crypto::key::ed25519::{PublicKey, Signature}; use pallas_crypto::vrf::{ VrfProof, VrfProofBytes, VrfProofHashBytes, VrfPublicKey, VrfPublicKeyBytes, }; @@ -22,8 +24,8 @@ static CERTIFIED_NATURAL_MAX: LazyLock = LazyLock::new(|| { /// Validator for a block using praos consensus. pub struct BlockValidator<'b> { - header: &'b babbage::Header, - pool_info: &'b dyn PoolInfo, + header: &'b babbage::MintedHeader<'b>, + ledger_state: &'b dyn LedgerState, epoch_nonce: &'b Hash<32>, // c is the ln(1-active_slots_coeff). Usually ln(1-0.05) c: &'b FixedDecimal, @@ -31,14 +33,14 @@ pub struct BlockValidator<'b> { impl<'b> BlockValidator<'b> { pub fn new( - header: &'b babbage::Header, - pool_info: &'b dyn PoolInfo, + header: &'b babbage::MintedHeader, + ledger_state: &'b dyn LedgerState, epoch_nonce: &'b Hash<32>, c: &'b FixedDecimal, ) -> Self { Self { header, - pool_info, + ledger_state, epoch_nonce, c, } @@ -49,6 +51,7 @@ impl<'b> BlockValidator<'b> { let _enter = span.enter(); // Grab all the values we need to validate the block + let absolute_slot = self.header.header_body.slot; let issuer_vkey = &self.header.header_body.issuer_vkey; let pool_id: PoolId = issuer_vkey_to_pool_id(issuer_vkey); let vrf_vkey: VrfPublicKeyBytes = match (&self.header.header_body.vrf_vkey).try_into() { @@ -58,11 +61,8 @@ impl<'b> BlockValidator<'b> { return false; } }; - if !self.ledger_matches_block_vrf_key_hash(&pool_id, &vrf_vkey) { - // Fail fast if the vrf key hash in the block does not match the ledger - return false; - } - let sigma: FixedDecimal = match self.pool_info.sigma(&pool_id) { + let leader_vrf_output = &self.header.header_body.leader_vrf_output(); + let sigma: FixedDecimal = match self.ledger_state.pool_id_to_sigma(&pool_id) { Ok(sigma) => { FixedDecimal::from(sigma.numerator) / FixedDecimal::from(sigma.denominator) } @@ -71,10 +71,6 @@ impl<'b> BlockValidator<'b> { return false; } }; - let absolute_slot = self.header.header_body.slot; - - // Get the leader VRF output hash from the block vrf result - let leader_vrf_output = &self.header.header_body.leader_vrf_output(); let block_vrf_proof_hash: VrfProofHashBytes = match (&self.header.header_body.vrf_result.0).try_into() { @@ -109,13 +105,166 @@ impl<'b> BlockValidator<'b> { ); trace!("kes_signature: {}", hex::encode(kes_signature)); + if !self.ledger_matches_block_vrf_key_hash(&pool_id, &vrf_vkey) { + // Fail fast if the vrf key hash in the block does not match the ledger + error!("VRF key hash validation failed"); + return false; + } + trace!("VRF key hash validated"); + + if !self.validate_block_vrf( + absolute_slot, + &vrf_vkey, + leader_vrf_output, + &sigma, + block_vrf_proof_hash, + &block_vrf_proof, + ) { + // Fail if the block VRF validation has failed + error!("Block VRF validation failed"); + return false; + } + trace!("Block VRF validated"); + + if !self.validate_operational_certificate(issuer_vkey.as_slice()) { + // Fail if the operational certificate validation has failed + error!("Operational Certificate signature validation failed"); + return false; + }; + trace!("Operational Certificate signature validated"); + + if !self.validate_kes_signature(absolute_slot, kes_signature) { + // Fail if the KES signature validation has failed + error!("KES signature validation failed"); + return false; + } + trace!("KES signature validated"); + true + } + + fn validate_kes_signature(&self, absolute_slot: u64, kes_signature: &[u8]) -> bool { + // Verify the KES signature + let kes_pk = match KesPublicKey::from_bytes( + &self + .header + .header_body + .operational_cert + .operational_cert_hot_vkey, + ) { + Ok(kes_pk) => kes_pk, + Err(error) => { + error!("Could not convert kes_pk: {}", error); + return false; + } + }; + + // calculate the right KES period to verify the signature + let slot_kes_period = self.ledger_state.slot_to_kes_period(absolute_slot); + let opcert_kes_period = self + .header + .header_body + .operational_cert + .operational_cert_kes_period; + + if opcert_kes_period > slot_kes_period { + error!("Operational Certificate KES period is greater than the block slot KES period!"); + return false; + } + if slot_kes_period >= opcert_kes_period + self.ledger_state.max_kes_evolutions() { + error!("Operational Certificate KES period is too old!"); + return false; + } + + let kes_period = (slot_kes_period - opcert_kes_period) as u32; + trace!("kes_period: {}", kes_period); + + // The header_body_cbor was signed by the KES private key. Verify this with the KES public key + let header_body_cbor = self.header.header_body.raw_cbor(); + let kes_signature = match KesSignature::from_bytes(kes_signature) { + Ok(kes_signature) => kes_signature, + Err(error) => { + error!("Could not convert kes_signature: {}", error); + return false; + } + }; + + match kes_signature.verify(kes_period, &kes_pk, header_body_cbor) { + Ok(_) => true, + Err(error) => { + error!("KES signature verification failed: {}", error); + false + } + } + } + + fn validate_operational_certificate(&self, issuer_vkey: &[u8]) -> bool { + // Verify the Operational Certificate signature + let opcert_signature = match Signature::try_from( + self.header + .header_body + .operational_cert + .operational_cert_sigma + .as_slice(), + ) { + Ok(opcert_signature) => opcert_signature, + Err(error) => { + error!("Could not convert opcert_signature: {}", error); + return false; + } + }; + let cold_pk = match PublicKey::try_from(issuer_vkey) { + Ok(cold_pk) => cold_pk, + Err(error) => { + error!("Could not convert cold_pk: {}", error); + return false; + } + }; + + // The opcert message is a concatenation of the KES vkey, the counter, and the kes period + let mut opcert_message = Vec::new(); + opcert_message.extend_from_slice( + &self + .header + .header_body + .operational_cert + .operational_cert_hot_vkey, + ); + opcert_message.extend_from_slice( + &self + .header + .header_body + .operational_cert + .operational_cert_sequence_number + .to_be_bytes(), + ); + opcert_message.extend_from_slice( + &self + .header + .header_body + .operational_cert + .operational_cert_kes_period + .to_be_bytes(), + ); + + cold_pk.verify(&opcert_message, &opcert_signature) + } + + fn validate_block_vrf( + &self, + absolute_slot: u64, + vrf_vkey: &VrfPublicKeyBytes, + leader_vrf_output: &Vec, + sigma: &FixedDecimal, + block_vrf_proof_hash: VrfProofHashBytes, + block_vrf_proof: &VrfProofBytes, + ) -> bool { // Calculate the VRF input seed so we can verify the VRF output against it. let vrf_input_seed = self.mk_vrf_input(absolute_slot, self.epoch_nonce.as_ref()); trace!("vrf_input_seed: {}", vrf_input_seed); // Verify the VRF proof - let vrf_proof = VrfProof::from(&block_vrf_proof); - let vrf_vkey = VrfPublicKey::from(&vrf_vkey); + let vrf_proof = VrfProof::from(block_vrf_proof); + let vrf_vkey = VrfPublicKey::from(vrf_vkey); match vrf_proof.verify(&vrf_vkey, vrf_input_seed.as_ref()) { Ok(proof_hash) => { if proof_hash.as_slice() != block_vrf_proof_hash.as_slice() { @@ -136,16 +285,11 @@ impl<'b> BlockValidator<'b> { } else { // The leader VRF output hash matches what was in the block // Now we need to check if the pool had enough sigma stake to produce this block - if self.pool_meets_delegation_threshold( - &sigma, + self.pool_meets_delegation_threshold( + sigma, absolute_slot, leader_vrf_output.as_slice(), - ) { - // TODO: Validate the KES signature - true - } else { - false - } + ) } } } @@ -186,11 +330,9 @@ impl<'b> BlockValidator<'b> { true } _ => { - trace!( + error!( "Slot: {} - NOT Leader: {} >= {}", - absolute_slot, - recip_q, - ordering.approx + absolute_slot, recip_q, ordering.approx ); false } @@ -205,7 +347,7 @@ impl<'b> BlockValidator<'b> { ) -> bool { let vrf_vkey_hash: Hash<32> = Hasher::<256>::hash(vrf_vkey); trace!("block vrf_vkey_hash: {}", hex::encode(vrf_vkey_hash)); - let ledger_vrf_vkey_hash = match self.pool_info.vrf_vkey_hash(pool_id) { + let ledger_vrf_vkey_hash = match self.ledger_state.vrf_vkey_hash(pool_id) { Ok(ledger_vrf_vkey_hash) => ledger_vrf_vkey_hash, Err(error) => { warn!("{:?} - {:?}", error, pool_id); @@ -243,7 +385,7 @@ mod tests { use crate::consensus::BlockValidator; use ctor::ctor; use mockall::predicate::eq; - use ouroboros::ledger::{MockPoolInfo, PoolId, PoolSigma}; + use ouroboros::ledger::{MockLedgerState, PoolId, PoolSigma}; use ouroboros::validator::Validator; use pallas_crypto::hash::Hash; use pallas_math::math::{FixedDecimal, FixedPrecision}; @@ -252,10 +394,9 @@ mod tests { #[ctor] fn init() { // set rust log level to TRACE - // std::env::set_var("RUST_LOG", "ouroboros-praos=trace"); - + // std::env::set_var("RUST_LOG", "trace"); // initialize tracing crate - tracing_subscriber::fmt::init(); + // tracing_subscriber::fmt::init(); } #[test] @@ -299,9 +440,9 @@ mod tests { let babbage_header = multi_era_header.as_babbage().expect("Infallible"); assert_eq!(babbage_header.header_body.slot, 134402628u64); - let mut pool_info = MockPoolInfo::new(); - pool_info - .expect_sigma() + let mut ledger_state = MockLedgerState::new(); + ledger_state + .expect_pool_id_to_sigma() .with(eq(pool_id)) .returning(move |_| { Ok(PoolSigma { @@ -309,12 +450,19 @@ mod tests { denominator, }) }); - pool_info + ledger_state .expect_vrf_vkey_hash() .with(eq(pool_id)) .returning(move |_| Ok(vrf_vkey_hash)); + ledger_state.expect_slot_to_kes_period().returning(|slot| { + // hardcode some values from shelley-genesis.json for the mock implementation + let slots_per_kes_period: u64 = 129600; // from shelley-genesis.json (1.5 days in seconds) + slot / slots_per_kes_period + }); + ledger_state.expect_max_kes_evolutions().returning(|| 62); - let block_validator = BlockValidator::new(babbage_header, &pool_info, &epoch_nonce, &c); + let block_validator = + BlockValidator::new(babbage_header, &ledger_state, &epoch_nonce, &c); assert_eq!(block_validator.validate(), expected); } } diff --git a/ouroboros/Cargo.toml b/ouroboros/Cargo.toml index 8ffc9c2..f512fb4 100644 --- a/ouroboros/Cargo.toml +++ b/ouroboros/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" [dependencies] hex = "0.4.3" mockall = "0.13" -pallas-codec = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } -pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" } +pallas-codec = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } +pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" } thiserror = "1.0" [dev-dependencies] diff --git a/ouroboros/src/ledger/mod.rs b/ouroboros/src/ledger/mod.rs index 52a5c8c..bd20823 100644 --- a/ouroboros/src/ledger/mod.rs +++ b/ouroboros/src/ledger/mod.rs @@ -20,16 +20,22 @@ pub struct PoolSigma { pub denominator: u64, } -/// The pool info trait provides a lookup mechanism for pool data. This is sourced from the ledger +/// The LedgerState trait provides a lookup mechanism for various information sourced from the ledger #[automock] -pub trait PoolInfo: Send + Sync { +pub trait LedgerState: Send + Sync { /// Performs a lookup of a pool_id to its sigma value. This usually represents a different set of /// sigma snapshot data depending on whether we need to look up the pool_id in the current epoch /// or in the future. - fn sigma(&self, pool_id: &PoolId) -> Result; + fn pool_id_to_sigma(&self, pool_id: &PoolId) -> Result; /// Hashes the vrf vkey of a pool. fn vrf_vkey_hash(&self, pool_id: &PoolId) -> Result, Error>; + + /// Calculate the KES period given an absolute slot and some shelley-genesis values + fn slot_to_kes_period(&self, slot: u64) -> u64; + + /// Get the maximum number of KES evolutions from the ledger state + fn max_kes_evolutions(&self) -> u64; } /// The node's cold vkey is hashed with blake2b224 to create the pool id