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 12, 2024
1 parent 36b4f01 commit 174c9b1
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 48 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 = "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"
226 changes: 187 additions & 39 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 @@ -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() {
Expand All @@ -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)
}
Expand All @@ -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() {
Expand Down Expand Up @@ -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<u8>,
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() {
Expand All @@ -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
}
)
}
}
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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);
Expand Down Expand Up @@ -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};
Expand All @@ -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]
Expand Down Expand Up @@ -299,22 +440,29 @@ 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(|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);
}
}
Expand Down
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 = "7f988a16d412d4891408f99785372a4ef617984d" }
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
thiserror = "1.0"

[dev-dependencies]
Expand Down
12 changes: 9 additions & 3 deletions ouroboros/src/ledger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;

/// 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
Expand Down

0 comments on commit 174c9b1

Please sign in to comment.