|
| 1 | +use acropolis_common::{ |
| 2 | + crypto::keyhash_224, |
| 3 | + validation::{ |
| 4 | + KesSignatureError, KesValidation, KesValidationError, OperationalCertificateError, |
| 5 | + }, |
| 6 | + GenesisDelegates, PoolId, |
| 7 | +}; |
| 8 | +use imbl::HashMap; |
| 9 | +use pallas::{crypto::key::ed25519, ledger::traverse::MultiEraHeader}; |
| 10 | + |
| 11 | +use crate::ouroboros::{kes, praos, tpraos}; |
| 12 | + |
| 13 | +#[derive(Copy, Clone)] |
| 14 | +pub struct OperationalCertificate<'a> { |
| 15 | + pub operational_cert_hot_vkey: &'a [u8], |
| 16 | + pub operational_cert_sequence_number: u64, |
| 17 | + pub operational_cert_kes_period: u64, |
| 18 | + pub operational_cert_sigma: &'a [u8], |
| 19 | +} |
| 20 | + |
| 21 | +pub fn validate_kes_signature( |
| 22 | + slot_kes_period: u64, |
| 23 | + opcert_kes_period: u64, |
| 24 | + header_body: &[u8], |
| 25 | + public_key: &kes::PublicKey, |
| 26 | + signature: &kes::Signature, |
| 27 | + max_kes_evolutions: u64, |
| 28 | +) -> Result<(), KesSignatureError> { |
| 29 | + if opcert_kes_period > slot_kes_period { |
| 30 | + return Err(KesSignatureError::KesBeforeStartOcert { |
| 31 | + ocert_start_period: opcert_kes_period, |
| 32 | + current_period: slot_kes_period, |
| 33 | + }); |
| 34 | + } |
| 35 | + |
| 36 | + if slot_kes_period >= opcert_kes_period + max_kes_evolutions { |
| 37 | + return Err(KesSignatureError::KesAfterEndOcert { |
| 38 | + current_period: slot_kes_period, |
| 39 | + ocert_start_period: opcert_kes_period, |
| 40 | + max_kes_evolutions, |
| 41 | + }); |
| 42 | + } |
| 43 | + |
| 44 | + let kes_period = (slot_kes_period - opcert_kes_period) as u32; |
| 45 | + |
| 46 | + signature.verify(kes_period, public_key, header_body).map_err(|error| { |
| 47 | + KesSignatureError::InvalidKesSignatureOcert { |
| 48 | + current_period: slot_kes_period, |
| 49 | + ocert_start_period: opcert_kes_period, |
| 50 | + reason: error.to_string(), |
| 51 | + } |
| 52 | + })?; |
| 53 | + |
| 54 | + Ok(()) |
| 55 | +} |
| 56 | + |
| 57 | +pub fn validate_operational_certificate<'a>( |
| 58 | + certificate: OperationalCertificate<'a>, |
| 59 | + issuer: &ed25519::PublicKey, |
| 60 | + latest_sequence_number: u64, |
| 61 | + is_praos: bool, |
| 62 | +) -> Result<(), OperationalCertificateError> { |
| 63 | + // Verify the Operational Certificate signature |
| 64 | + let signature = |
| 65 | + ed25519::Signature::try_from(certificate.operational_cert_sigma).map_err(|error| { |
| 66 | + OperationalCertificateError::MalformedSignatureOcert { |
| 67 | + reason: error.to_string(), |
| 68 | + } |
| 69 | + })?; |
| 70 | + |
| 71 | + let declared_sequence_number = certificate.operational_cert_sequence_number; |
| 72 | + |
| 73 | + // Check the sequence number of the operational certificate. It should either be the same |
| 74 | + // as the latest known sequence number for the issuer or one greater. |
| 75 | + if declared_sequence_number < latest_sequence_number { |
| 76 | + return Err(OperationalCertificateError::CounterTooSmallOcert { |
| 77 | + latest_counter: latest_sequence_number, |
| 78 | + declared_counter: declared_sequence_number, |
| 79 | + }); |
| 80 | + } |
| 81 | + |
| 82 | + // this is only for praos protocol |
| 83 | + if is_praos && (declared_sequence_number - latest_sequence_number) > 1 { |
| 84 | + return Err(OperationalCertificateError::CounterOverIncrementedOcert { |
| 85 | + latest_counter: latest_sequence_number, |
| 86 | + declared_counter: declared_sequence_number, |
| 87 | + }); |
| 88 | + } |
| 89 | + |
| 90 | + // The opcert message is a concatenation of the KES vkey, the sequence number, and the kes period |
| 91 | + let mut message = Vec::new(); |
| 92 | + message.extend_from_slice(certificate.operational_cert_hot_vkey); |
| 93 | + message.extend_from_slice(&certificate.operational_cert_sequence_number.to_be_bytes()); |
| 94 | + message.extend_from_slice(&certificate.operational_cert_kes_period.to_be_bytes()); |
| 95 | + if !issuer.verify(&message, &signature) { |
| 96 | + return Err(OperationalCertificateError::InvalidSignatureOcert { |
| 97 | + issuer: issuer.as_ref().to_vec(), |
| 98 | + }); |
| 99 | + } |
| 100 | + |
| 101 | + Ok(()) |
| 102 | +} |
| 103 | + |
| 104 | +pub fn validate_block_kes<'a>( |
| 105 | + header: &'a MultiEraHeader, |
| 106 | + ocert_counters: &'a HashMap<PoolId, u64>, |
| 107 | + active_spos: &'a [PoolId], |
| 108 | + genesis_delegs: &'a GenesisDelegates, |
| 109 | + slots_per_kes_period: u64, |
| 110 | + max_kes_evolutions: u64, |
| 111 | +) -> Result<Vec<KesValidation<'a>>, Box<KesValidationError>> { |
| 112 | + let is_praos = matches!(header, MultiEraHeader::BabbageCompatible(_)); |
| 113 | + |
| 114 | + let issuer_vkey = header.issuer_vkey().ok_or(Box::new(KesValidationError::Other( |
| 115 | + "Issuer Key is not set".to_string(), |
| 116 | + )))?; |
| 117 | + let issuer = ed25519::PublicKey::from( |
| 118 | + <[u8; ed25519::PublicKey::SIZE]>::try_from(issuer_vkey) |
| 119 | + .map_err(|_| Box::new(KesValidationError::Other("Invalid issuer key".to_string())))?, |
| 120 | + ); |
| 121 | + let pool_id = PoolId::from(keyhash_224(issuer_vkey)); |
| 122 | + |
| 123 | + let slot_kes_period = header.slot() / slots_per_kes_period; |
| 124 | + let cert = operational_cert(header).ok_or(Box::new(KesValidationError::Other( |
| 125 | + "Operational certificate is not set".to_string(), |
| 126 | + )))?; |
| 127 | + let body_sig = body_signature(header).ok_or(Box::new(KesValidationError::Other( |
| 128 | + "Body signature is not set".to_string(), |
| 129 | + )))?; |
| 130 | + let raw_header_body = header.header_body_cbor().ok_or(Box::new(KesValidationError::Other( |
| 131 | + "Header body is not set".to_string(), |
| 132 | + )))?; |
| 133 | + |
| 134 | + let latest_sequence_number = if is_praos { |
| 135 | + praos::latest_issue_no_praos(ocert_counters, active_spos, &pool_id) |
| 136 | + } else { |
| 137 | + tpraos::latest_issue_no_tpraos(ocert_counters, active_spos, genesis_delegs, &pool_id) |
| 138 | + } |
| 139 | + .ok_or(Box::new(KesValidationError::NoOCertCounter { pool_id }))?; |
| 140 | + |
| 141 | + Ok(vec![ |
| 142 | + Box::new(move || { |
| 143 | + validate_kes_signature( |
| 144 | + slot_kes_period, |
| 145 | + cert.operational_cert_kes_period, |
| 146 | + raw_header_body, |
| 147 | + &kes::PublicKey::try_from(cert.operational_cert_hot_vkey).map_err(|_| { |
| 148 | + KesValidationError::Other( |
| 149 | + "Invalid operational certificate hot vkey".to_string(), |
| 150 | + ) |
| 151 | + })?, |
| 152 | + &kes::Signature::try_from(body_sig) |
| 153 | + .map_err(|_| KesValidationError::Other("Invalid body signature".to_string()))?, |
| 154 | + max_kes_evolutions, |
| 155 | + )?; |
| 156 | + Ok(()) |
| 157 | + }), |
| 158 | + Box::new(move || { |
| 159 | + validate_operational_certificate(cert, &issuer, latest_sequence_number, is_praos)?; |
| 160 | + Ok(()) |
| 161 | + }), |
| 162 | + ]) |
| 163 | +} |
| 164 | + |
| 165 | +fn operational_cert<'a>(header: &'a MultiEraHeader) -> Option<OperationalCertificate<'a>> { |
| 166 | + match header { |
| 167 | + MultiEraHeader::ShelleyCompatible(x) => { |
| 168 | + let cert = OperationalCertificate { |
| 169 | + operational_cert_hot_vkey: &x.header_body.operational_cert_hot_vkey, |
| 170 | + operational_cert_sequence_number: x.header_body.operational_cert_sequence_number, |
| 171 | + operational_cert_kes_period: x.header_body.operational_cert_kes_period, |
| 172 | + operational_cert_sigma: &x.header_body.operational_cert_sigma, |
| 173 | + }; |
| 174 | + Some(cert) |
| 175 | + } |
| 176 | + MultiEraHeader::BabbageCompatible(x) => Some(OperationalCertificate { |
| 177 | + operational_cert_hot_vkey: &x.header_body.operational_cert.operational_cert_hot_vkey, |
| 178 | + operational_cert_sequence_number: x |
| 179 | + .header_body |
| 180 | + .operational_cert |
| 181 | + .operational_cert_sequence_number, |
| 182 | + operational_cert_kes_period: x.header_body.operational_cert.operational_cert_kes_period, |
| 183 | + operational_cert_sigma: &x.header_body.operational_cert.operational_cert_sigma, |
| 184 | + }), |
| 185 | + _ => None, |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +fn body_signature<'a>(header: &'a MultiEraHeader) -> Option<&'a [u8]> { |
| 190 | + match header { |
| 191 | + MultiEraHeader::ShelleyCompatible(x) => Some(&x.body_signature), |
| 192 | + MultiEraHeader::BabbageCompatible(x) => Some(&x.body_signature), |
| 193 | + _ => None, |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +#[cfg(test)] |
| 198 | +mod tests { |
| 199 | + use acropolis_common::{genesis_values::GenesisValues, Era}; |
| 200 | + use pallas::ledger::traverse::MultiEraHeader; |
| 201 | + |
| 202 | + use super::*; |
| 203 | + |
| 204 | + #[test] |
| 205 | + fn test_4490511_block_produced_by_genesis_key() { |
| 206 | + let slots_per_kes_period = 129600; |
| 207 | + let max_kes_evolutions = 62; |
| 208 | + let genesis_values = GenesisValues::mainnet(); |
| 209 | + |
| 210 | + let block_header_4490511: Vec<u8> = |
| 211 | + hex::decode(include_str!("./data/4490511.cbor")).unwrap(); |
| 212 | + let block_header = |
| 213 | + MultiEraHeader::decode(Era::Shelley as u8, None, &block_header_4490511).unwrap(); |
| 214 | + |
| 215 | + let ocert_counters = HashMap::new(); |
| 216 | + let active_spos = Vec::new(); |
| 217 | + |
| 218 | + let result = validate_block_kes( |
| 219 | + &block_header, |
| 220 | + &ocert_counters, |
| 221 | + &active_spos, |
| 222 | + &genesis_values.genesis_delegs, |
| 223 | + slots_per_kes_period, |
| 224 | + max_kes_evolutions, |
| 225 | + ) |
| 226 | + .and_then(|kes_validations| { |
| 227 | + kes_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) |
| 228 | + }); |
| 229 | + assert!(result.is_ok()); |
| 230 | + } |
| 231 | +} |
0 commit comments