Skip to content
This repository was archived by the owner on Feb 6, 2025. It is now read-only.

Commit 174c9b1

Browse files
feat: Validate conway block KES signatures
1 parent 36b4f01 commit 174c9b1

File tree

4 files changed

+202
-48
lines changed

4 files changed

+202
-48
lines changed

ouroboros-praos/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ edition = "2021"
66
[dependencies]
77
hex = "0.4"
88
ouroboros = { path = "../ouroboros" }
9-
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
10-
pallas-math = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
11-
pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
9+
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
10+
pallas-math = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
11+
pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
1212
tracing = "0.1"
1313

1414
[dev-dependencies]
1515
ctor = "0.2"
1616
insta = { version = "1.40.0", features = ["yaml"] }
1717
mockall = "0.13"
18-
pallas-traverse = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
18+
pallas-traverse = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
1919
tracing-subscriber = "0.3"

ouroboros-praos/src/consensus/mod.rs

+187-39
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use ouroboros::ledger::{issuer_vkey_to_pool_id, PoolId, PoolInfo};
1+
use ouroboros::ledger::{issuer_vkey_to_pool_id, LedgerState, PoolId};
22
use ouroboros::validator::Validator;
33
use pallas_crypto::hash::{Hash, Hasher};
4+
use pallas_crypto::kes::{KesPublicKey, KesSignature};
5+
use pallas_crypto::key::ed25519::{PublicKey, Signature};
46
use pallas_crypto::vrf::{
57
VrfProof, VrfProofBytes, VrfProofHashBytes, VrfPublicKey, VrfPublicKeyBytes,
68
};
@@ -22,23 +24,23 @@ static CERTIFIED_NATURAL_MAX: LazyLock<FixedDecimal> = LazyLock::new(|| {
2224

2325
/// Validator for a block using praos consensus.
2426
pub struct BlockValidator<'b> {
25-
header: &'b babbage::Header,
26-
pool_info: &'b dyn PoolInfo,
27+
header: &'b babbage::MintedHeader<'b>,
28+
ledger_state: &'b dyn LedgerState,
2729
epoch_nonce: &'b Hash<32>,
2830
// c is the ln(1-active_slots_coeff). Usually ln(1-0.05)
2931
c: &'b FixedDecimal,
3032
}
3133

3234
impl<'b> BlockValidator<'b> {
3335
pub fn new(
34-
header: &'b babbage::Header,
35-
pool_info: &'b dyn PoolInfo,
36+
header: &'b babbage::MintedHeader,
37+
ledger_state: &'b dyn LedgerState,
3638
epoch_nonce: &'b Hash<32>,
3739
c: &'b FixedDecimal,
3840
) -> Self {
3941
Self {
4042
header,
41-
pool_info,
43+
ledger_state,
4244
epoch_nonce,
4345
c,
4446
}
@@ -49,6 +51,7 @@ impl<'b> BlockValidator<'b> {
4951
let _enter = span.enter();
5052

5153
// Grab all the values we need to validate the block
54+
let absolute_slot = self.header.header_body.slot;
5255
let issuer_vkey = &self.header.header_body.issuer_vkey;
5356
let pool_id: PoolId = issuer_vkey_to_pool_id(issuer_vkey);
5457
let vrf_vkey: VrfPublicKeyBytes = match (&self.header.header_body.vrf_vkey).try_into() {
@@ -58,11 +61,8 @@ impl<'b> BlockValidator<'b> {
5861
return false;
5962
}
6063
};
61-
if !self.ledger_matches_block_vrf_key_hash(&pool_id, &vrf_vkey) {
62-
// Fail fast if the vrf key hash in the block does not match the ledger
63-
return false;
64-
}
65-
let sigma: FixedDecimal = match self.pool_info.sigma(&pool_id) {
64+
let leader_vrf_output = &self.header.header_body.leader_vrf_output();
65+
let sigma: FixedDecimal = match self.ledger_state.pool_id_to_sigma(&pool_id) {
6666
Ok(sigma) => {
6767
FixedDecimal::from(sigma.numerator) / FixedDecimal::from(sigma.denominator)
6868
}
@@ -71,10 +71,6 @@ impl<'b> BlockValidator<'b> {
7171
return false;
7272
}
7373
};
74-
let absolute_slot = self.header.header_body.slot;
75-
76-
// Get the leader VRF output hash from the block vrf result
77-
let leader_vrf_output = &self.header.header_body.leader_vrf_output();
7874

7975
let block_vrf_proof_hash: VrfProofHashBytes =
8076
match (&self.header.header_body.vrf_result.0).try_into() {
@@ -109,13 +105,166 @@ impl<'b> BlockValidator<'b> {
109105
);
110106
trace!("kes_signature: {}", hex::encode(kes_signature));
111107

108+
if !self.ledger_matches_block_vrf_key_hash(&pool_id, &vrf_vkey) {
109+
// Fail fast if the vrf key hash in the block does not match the ledger
110+
error!("VRF key hash validation failed");
111+
return false;
112+
}
113+
trace!("VRF key hash validated");
114+
115+
if !self.validate_block_vrf(
116+
absolute_slot,
117+
&vrf_vkey,
118+
leader_vrf_output,
119+
&sigma,
120+
block_vrf_proof_hash,
121+
&block_vrf_proof,
122+
) {
123+
// Fail if the block VRF validation has failed
124+
error!("Block VRF validation failed");
125+
return false;
126+
}
127+
trace!("Block VRF validated");
128+
129+
if !self.validate_operational_certificate(issuer_vkey.as_slice()) {
130+
// Fail if the operational certificate validation has failed
131+
error!("Operational Certificate signature validation failed");
132+
return false;
133+
};
134+
trace!("Operational Certificate signature validated");
135+
136+
if !self.validate_kes_signature(absolute_slot, kes_signature) {
137+
// Fail if the KES signature validation has failed
138+
error!("KES signature validation failed");
139+
return false;
140+
}
141+
trace!("KES signature validated");
142+
true
143+
}
144+
145+
fn validate_kes_signature(&self, absolute_slot: u64, kes_signature: &[u8]) -> bool {
146+
// Verify the KES signature
147+
let kes_pk = match KesPublicKey::from_bytes(
148+
&self
149+
.header
150+
.header_body
151+
.operational_cert
152+
.operational_cert_hot_vkey,
153+
) {
154+
Ok(kes_pk) => kes_pk,
155+
Err(error) => {
156+
error!("Could not convert kes_pk: {}", error);
157+
return false;
158+
}
159+
};
160+
161+
// calculate the right KES period to verify the signature
162+
let slot_kes_period = self.ledger_state.slot_to_kes_period(absolute_slot);
163+
let opcert_kes_period = self
164+
.header
165+
.header_body
166+
.operational_cert
167+
.operational_cert_kes_period;
168+
169+
if opcert_kes_period > slot_kes_period {
170+
error!("Operational Certificate KES period is greater than the block slot KES period!");
171+
return false;
172+
}
173+
if slot_kes_period >= opcert_kes_period + self.ledger_state.max_kes_evolutions() {
174+
error!("Operational Certificate KES period is too old!");
175+
return false;
176+
}
177+
178+
let kes_period = (slot_kes_period - opcert_kes_period) as u32;
179+
trace!("kes_period: {}", kes_period);
180+
181+
// The header_body_cbor was signed by the KES private key. Verify this with the KES public key
182+
let header_body_cbor = self.header.header_body.raw_cbor();
183+
let kes_signature = match KesSignature::from_bytes(kes_signature) {
184+
Ok(kes_signature) => kes_signature,
185+
Err(error) => {
186+
error!("Could not convert kes_signature: {}", error);
187+
return false;
188+
}
189+
};
190+
191+
match kes_signature.verify(kes_period, &kes_pk, header_body_cbor) {
192+
Ok(_) => true,
193+
Err(error) => {
194+
error!("KES signature verification failed: {}", error);
195+
false
196+
}
197+
}
198+
}
199+
200+
fn validate_operational_certificate(&self, issuer_vkey: &[u8]) -> bool {
201+
// Verify the Operational Certificate signature
202+
let opcert_signature = match Signature::try_from(
203+
self.header
204+
.header_body
205+
.operational_cert
206+
.operational_cert_sigma
207+
.as_slice(),
208+
) {
209+
Ok(opcert_signature) => opcert_signature,
210+
Err(error) => {
211+
error!("Could not convert opcert_signature: {}", error);
212+
return false;
213+
}
214+
};
215+
let cold_pk = match PublicKey::try_from(issuer_vkey) {
216+
Ok(cold_pk) => cold_pk,
217+
Err(error) => {
218+
error!("Could not convert cold_pk: {}", error);
219+
return false;
220+
}
221+
};
222+
223+
// The opcert message is a concatenation of the KES vkey, the counter, and the kes period
224+
let mut opcert_message = Vec::new();
225+
opcert_message.extend_from_slice(
226+
&self
227+
.header
228+
.header_body
229+
.operational_cert
230+
.operational_cert_hot_vkey,
231+
);
232+
opcert_message.extend_from_slice(
233+
&self
234+
.header
235+
.header_body
236+
.operational_cert
237+
.operational_cert_sequence_number
238+
.to_be_bytes(),
239+
);
240+
opcert_message.extend_from_slice(
241+
&self
242+
.header
243+
.header_body
244+
.operational_cert
245+
.operational_cert_kes_period
246+
.to_be_bytes(),
247+
);
248+
249+
cold_pk.verify(&opcert_message, &opcert_signature)
250+
}
251+
252+
fn validate_block_vrf(
253+
&self,
254+
absolute_slot: u64,
255+
vrf_vkey: &VrfPublicKeyBytes,
256+
leader_vrf_output: &Vec<u8>,
257+
sigma: &FixedDecimal,
258+
block_vrf_proof_hash: VrfProofHashBytes,
259+
block_vrf_proof: &VrfProofBytes,
260+
) -> bool {
112261
// Calculate the VRF input seed so we can verify the VRF output against it.
113262
let vrf_input_seed = self.mk_vrf_input(absolute_slot, self.epoch_nonce.as_ref());
114263
trace!("vrf_input_seed: {}", vrf_input_seed);
115264

116265
// Verify the VRF proof
117-
let vrf_proof = VrfProof::from(&block_vrf_proof);
118-
let vrf_vkey = VrfPublicKey::from(&vrf_vkey);
266+
let vrf_proof = VrfProof::from(block_vrf_proof);
267+
let vrf_vkey = VrfPublicKey::from(vrf_vkey);
119268
match vrf_proof.verify(&vrf_vkey, vrf_input_seed.as_ref()) {
120269
Ok(proof_hash) => {
121270
if proof_hash.as_slice() != block_vrf_proof_hash.as_slice() {
@@ -136,16 +285,11 @@ impl<'b> BlockValidator<'b> {
136285
} else {
137286
// The leader VRF output hash matches what was in the block
138287
// Now we need to check if the pool had enough sigma stake to produce this block
139-
if self.pool_meets_delegation_threshold(
140-
&sigma,
288+
self.pool_meets_delegation_threshold(
289+
sigma,
141290
absolute_slot,
142291
leader_vrf_output.as_slice(),
143-
) {
144-
// TODO: Validate the KES signature
145-
true
146-
} else {
147-
false
148-
}
292+
)
149293
}
150294
}
151295
}
@@ -186,11 +330,9 @@ impl<'b> BlockValidator<'b> {
186330
true
187331
}
188332
_ => {
189-
trace!(
333+
error!(
190334
"Slot: {} - NOT Leader: {} >= {}",
191-
absolute_slot,
192-
recip_q,
193-
ordering.approx
335+
absolute_slot, recip_q, ordering.approx
194336
);
195337
false
196338
}
@@ -205,7 +347,7 @@ impl<'b> BlockValidator<'b> {
205347
) -> bool {
206348
let vrf_vkey_hash: Hash<32> = Hasher::<256>::hash(vrf_vkey);
207349
trace!("block vrf_vkey_hash: {}", hex::encode(vrf_vkey_hash));
208-
let ledger_vrf_vkey_hash = match self.pool_info.vrf_vkey_hash(pool_id) {
350+
let ledger_vrf_vkey_hash = match self.ledger_state.vrf_vkey_hash(pool_id) {
209351
Ok(ledger_vrf_vkey_hash) => ledger_vrf_vkey_hash,
210352
Err(error) => {
211353
warn!("{:?} - {:?}", error, pool_id);
@@ -243,7 +385,7 @@ mod tests {
243385
use crate::consensus::BlockValidator;
244386
use ctor::ctor;
245387
use mockall::predicate::eq;
246-
use ouroboros::ledger::{MockPoolInfo, PoolId, PoolSigma};
388+
use ouroboros::ledger::{MockLedgerState, PoolId, PoolSigma};
247389
use ouroboros::validator::Validator;
248390
use pallas_crypto::hash::Hash;
249391
use pallas_math::math::{FixedDecimal, FixedPrecision};
@@ -252,10 +394,9 @@ mod tests {
252394
#[ctor]
253395
fn init() {
254396
// set rust log level to TRACE
255-
// std::env::set_var("RUST_LOG", "ouroboros-praos=trace");
256-
397+
// std::env::set_var("RUST_LOG", "trace");
257398
// initialize tracing crate
258-
tracing_subscriber::fmt::init();
399+
// tracing_subscriber::fmt::init();
259400
}
260401

261402
#[test]
@@ -299,22 +440,29 @@ mod tests {
299440
let babbage_header = multi_era_header.as_babbage().expect("Infallible");
300441
assert_eq!(babbage_header.header_body.slot, 134402628u64);
301442

302-
let mut pool_info = MockPoolInfo::new();
303-
pool_info
304-
.expect_sigma()
443+
let mut ledger_state = MockLedgerState::new();
444+
ledger_state
445+
.expect_pool_id_to_sigma()
305446
.with(eq(pool_id))
306447
.returning(move |_| {
307448
Ok(PoolSigma {
308449
numerator,
309450
denominator,
310451
})
311452
});
312-
pool_info
453+
ledger_state
313454
.expect_vrf_vkey_hash()
314455
.with(eq(pool_id))
315456
.returning(move |_| Ok(vrf_vkey_hash));
457+
ledger_state.expect_slot_to_kes_period().returning(|slot| {
458+
// hardcode some values from shelley-genesis.json for the mock implementation
459+
let slots_per_kes_period: u64 = 129600; // from shelley-genesis.json (1.5 days in seconds)
460+
slot / slots_per_kes_period
461+
});
462+
ledger_state.expect_max_kes_evolutions().returning(|| 62);
316463

317-
let block_validator = BlockValidator::new(babbage_header, &pool_info, &epoch_nonce, &c);
464+
let block_validator =
465+
BlockValidator::new(babbage_header, &ledger_state, &epoch_nonce, &c);
318466
assert_eq!(block_validator.validate(), expected);
319467
}
320468
}

ouroboros/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ edition = "2021"
66
[dependencies]
77
hex = "0.4.3"
88
mockall = "0.13"
9-
pallas-codec = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
10-
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "252714d58a93d58bf2cd7ada1156e3ace10a8298" }
9+
pallas-codec = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
10+
pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "7f988a16d412d4891408f99785372a4ef617984d" }
1111
thiserror = "1.0"
1212

1313
[dev-dependencies]

ouroboros/src/ledger/mod.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,22 @@ pub struct PoolSigma {
2020
pub denominator: u64,
2121
}
2222

23-
/// The pool info trait provides a lookup mechanism for pool data. This is sourced from the ledger
23+
/// The LedgerState trait provides a lookup mechanism for various information sourced from the ledger
2424
#[automock]
25-
pub trait PoolInfo: Send + Sync {
25+
pub trait LedgerState: Send + Sync {
2626
/// Performs a lookup of a pool_id to its sigma value. This usually represents a different set of
2727
/// sigma snapshot data depending on whether we need to look up the pool_id in the current epoch
2828
/// or in the future.
29-
fn sigma(&self, pool_id: &PoolId) -> Result<PoolSigma, Error>;
29+
fn pool_id_to_sigma(&self, pool_id: &PoolId) -> Result<PoolSigma, Error>;
3030

3131
/// Hashes the vrf vkey of a pool.
3232
fn vrf_vkey_hash(&self, pool_id: &PoolId) -> Result<Hash<32>, Error>;
33+
34+
/// Calculate the KES period given an absolute slot and some shelley-genesis values
35+
fn slot_to_kes_period(&self, slot: u64) -> u64;
36+
37+
/// Get the maximum number of KES evolutions from the ledger state
38+
fn max_kes_evolutions(&self) -> u64;
3339
}
3440

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

0 commit comments

Comments
 (0)