Skip to content

Commit

Permalink
feat: Validate conway block KES signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewWestberg committed Oct 11, 2024
1 parent c9fefdb commit 2f0a772
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 32 deletions.
8 changes: 4 additions & 4 deletions ouroboros-praos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "6829dd08ff92640a656e3558f08cc2501dea8314" }
pallas-math = { git = "https://github.com/txpipe/pallas", rev = "6829dd08ff92640a656e3558f08cc2501dea8314" }
pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "6829dd08ff92640a656e3558f08cc2501dea8314" }
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 = "6829dd08ff92640a656e3558f08cc2501dea8314" }
tracing-subscriber = "0.3"
229 changes: 206 additions & 23 deletions ouroboros-praos/src/consensus/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -22,23 +24,23 @@ static CERTIFIED_NATURAL_MAX: LazyLock<FixedDecimal> = 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,
}

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,
}
Expand All @@ -62,7 +64,7 @@ impl<'b> BlockValidator<'b> {
// 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 sigma: FixedDecimal = match self.ledger_state.pool_id_to_sigma(&pool_id) {
Ok(sigma) => {
FixedDecimal::from(sigma.numerator) / FixedDecimal::from(sigma.denominator)
}
Expand Down Expand Up @@ -116,7 +118,7 @@ impl<'b> BlockValidator<'b> {
// Verify the VRF proof
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()) {
let vrf_verified = 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() {
error!("VRF proof hash mismatch");
Expand All @@ -136,23 +138,122 @@ 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(
self.pool_meets_delegation_threshold(
&sigma,
absolute_slot,
leader_vrf_output.as_slice(),
) {
// TODO: Validate the KES signature
true
} else {
false
}
)
}
}
}
Err(error) => {
error!("Could not verify block vrf: {}", error);
false
}
};

if vrf_verified {
// 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;
}
};

// 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(),
);

let cold_pk = match PublicKey::try_from(issuer_vkey.as_slice()) {
Ok(cold_pk) => cold_pk,
Err(error) => {
error!("Could not convert cold_pk: {}", error);
return false;
}
};
let opcert_verified = cold_pk.verify(&opcert_message, &opcert_signature);
if opcert_verified {
trace!("Operational Certificate signature verified");
// 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 kes_period = (slot_kes_period
- self
.header
.header_body
.operational_cert
.operational_cert_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(_) => {
trace!("KES signature verified!");
true
}
Err(error) => {
error!("KES signature verification failed: {}", error);
false
}
}
} else {
error!("Operational Certificate signature verification failed");
false
}
} else {
false
}
}

Expand Down Expand Up @@ -205,7 +306,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);
Expand Down Expand Up @@ -243,7 +344,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};
Expand All @@ -252,7 +353,7 @@ 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();
Expand Down Expand Up @@ -299,23 +400,105 @@ 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 {
numerator,
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(move |slot| {
// hardcode some values from shelley-genesis.json for the mock implementation
let slot_length: u64 = 1; // from shelley-genesis.json (1 second)
let slots_per_kes_period: u64 = 129600; // from shelley-genesis.json (1.5 days in seconds)
slot / (slots_per_kes_period * slot_length)
});

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);
}
}
}
// // utility function to calculate the KES period given a slot and some genesis values
// fn slot_to_kes_period(slot: u64) -> u64 {
// let slot_length = 1u64; // from shelley-genesis.json (1 second)
// let slots_per_kes_period = 129600u64; // from shelley-genesis.json (1.5 days in seconds)
// slot / (slots_per_kes_period * slot_length)
// }
//
// #[test]
// fn kes_key_block_verification() {
// let test_block = include_bytes!("tests/mainnet_blockheader_10817298.cbor");
// let conway_block_tag: u8 = 6;
// let multi_era_header = MultiEraHeader::decode(conway_block_tag, None, test_block).unwrap();
// let header_hash = multi_era_header.hash();
// println!("header_hash: {}", hex::encode(header_hash.as_ref()));
// assert_eq!(
// hex::encode(header_hash),
// "627ea281970fc48f033c2d50d0a3393af5015ec6aaa0af435d8f2877173156ce"
// );
//
// let babbage_header = multi_era_header.as_babbage().expect("Infallible");
// assert_eq!(babbage_header.header_body.slot, 134402628u64);
// assert_eq!(
// hex::encode(
// &babbage_header
// .header_body
// .operational_cert
// .operational_cert_hot_vkey
// .as_slice()
// ),
// "2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292"
// );
//
// // We needed MintedHeader to be able to extract the header_body_cbor
// let header_body_cbor: &[u8] = multi_era_header.header_body_cbor().expect("Infallible");
// println!("header_body_cbor: {}", hex::encode(header_body_cbor));
// // let header_body_cbor = hex::decode("8a1a00a50f121a0802d24458203deea82abe788d260b8987a522aadec86c9f098e88a57d7cfcdb24f474a7afb65820cad3c900ca6baee9e65bf61073d900bfbca458eeca6d0b9f9931f5b1017a8cd65820576d49e98adfab65623dc16f9fff2edd210e8dd1d4588bfaf8af250beda9d3c7825840d944b8c81000fc1182ec02194ca9eca510fd84995d22bfe1842190b39d468e5ecbd863969e0c717b0071a371f748d44c895fa9233094cefcd3107410baabb19a5850f2a29f985d37ca8eb671c2847fab9cc45c93738a430b4e43837e7f33028b190a7e55152b0e901548961a66d56eebe72d616f9e68fd13e9955ccd8611c201a5b422ac8ef56af74cb657b5b868ce9d850f1945d15820639d4986d17de3cac8079a3b25d671f339467aa3a9948e29992dafebf90f719f8458202e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292171903e958401feeeabc7460b19370f4050e986b558b149fdc8724b4a4805af8fe45c8e7a7c6753894ad7a1b9c313da269ddc5922e150da3b378977f1dfea79fc52fd2c12f08820901").unwrap();
//
// // TODO: Verify the opcert signature by the node's cold key
// // ...
// // ...
//
// let kes_pk_bytes = babbage_header
// .header_body
// .operational_cert
// .operational_cert_hot_vkey
// .as_slice();
// assert_eq!(
// hex::encode(kes_pk_bytes),
// "2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292"
// );
// let kes_pk = PublicKey::from_bytes(kes_pk_bytes).unwrap();
// // let kes_pk = PublicKey::from_bytes(&hex::decode("2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292").unwrap()).unwrap();
//
// // calculate the right period to verify the signature
// let opcert_kes_period = babbage_header
// .header_body
// .operational_cert
// .operational_cert_kes_period;
// assert_eq!(opcert_kes_period, 1001u64);
// let slot_kes_period = slot_to_kes_period(babbage_header.header_body.slot);
// assert_eq!(slot_kes_period, 1037u64);
// let kes_period = (slot_kes_period - opcert_kes_period) as u32;
// assert_eq!(kes_period, 36u32);
//
// let signature = Sum6KesSig::from_bytes(babbage_header.body_signature.as_slice()).unwrap();
// assert!(
// signature
// .verify(kes_period, &kes_pk, header_body_cbor)
// .is_ok(),
// "Signature verification failed"
// );
// // assert!(signature.verify(kes_period, &kes_pk, header_body_cbor.as_slice()).is_ok(), "Signature verification failed");
// }
4 changes: 2 additions & 2 deletions ouroboros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "6829dd08ff92640a656e3558f08cc2501dea8314" }
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "6829dd08ff92640a656e3558f08cc2501dea8314" }
thiserror = "1.0"

[dev-dependencies]
Expand Down
9 changes: 6 additions & 3 deletions ouroboros/src/ledger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ 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<PoolSigma, Error>;
fn pool_id_to_sigma(&self, pool_id: &PoolId) -> Result<PoolSigma, Error>;

/// Hashes the vrf vkey of a pool.
fn vrf_vkey_hash(&self, pool_id: &PoolId) -> Result<Hash<32>, Error>;

/// Calculate the KES period given an absolute slot and some shelley-genesis values
fn slot_to_kes_period(&self, slot: u64) -> u64;
}

/// The node's cold vkey is hashed with blake2b224 to create the pool id
Expand Down

0 comments on commit 2f0a772

Please sign in to comment.